From e8a27bb203fe2aff70390a5eca002d7438da9b0f Mon Sep 17 00:00:00 2001
From: mrDarker <mr.darker@163.com>
Date: 星期三, 22 十月 2025 14:24:34 +0800
Subject: [PATCH] Merge branch 'clh' into liuyang

---
 SourceCode/Bond/DAQBridge/buffer/BufferRegistry.h   |   24 
 SourceCode/Bond/Servo/CCarrierSlotGrid.cpp          | 1083 ++++++
 SourceCode/Bond/Servo/CControlJobDlg.h              |   10 
 SourceCode/Bond/DAQBridge/core/Collector.h          |  163 
 SourceCode/Bond/Servo/GlassLogDb.h                  |    3 
 SourceCode/Bond/Servo/ProcessJob.cpp                |   94 
 SourceCode/Bond/Servo/CVacuumBake.cpp               |   53 
 SourceCode/Bond/Servo/CControlJob.h                 |    8 
 SourceCode/Bond/Servo/CJobDataS.cpp                 |    4 
 SourceCode/Bond/Servo/GroupLabel.h                  |   46 
 SourceCode/Bond/Servo/GlassLogDb.cpp                |   52 
 SourceCode/Bond/Servo/GlassJson.cpp                 |   65 
 SourceCode/Bond/Servo/CBakeCooling.cpp              |   48 
 SourceCode/Bond/Servo/Configuration.h               |    1 
 SourceCode/Bond/DAQBridge/proto/ProtocolCodec.cpp   |  663 +++
 SourceCode/Bond/Servo/ToolUnits.cpp                 |   20 
 SourceCode/Bond/DAQBridge/core/Display.cpp          |  266 +
 SourceCode/Bond/Servo/ClientListDlg.h               |   47 
 SourceCode/Bond/DAQBridge/core/DataTypes.h          |    9 
 SourceCode/Bond/Servo/CCarrierSlotSelector.h        |  122 
 SourceCode/Bond/Servo/Servo.rc                      |    0 
 SourceCode/Bond/DAQBridge/proto/Protocol.h          |  168 +
 SourceCode/Bond/DAQBridge/core/Display.h            |  113 
 SourceCode/Bond/Servo/CBonder.h                     |    2 
 SourceCode/Bond/DAQBridge/net/FrameAssembler.h      |   58 
 SourceCode/Bond/DAQBridge/buffer/SampleBuffer.h     |   57 
 SourceCode/Bond/Servo/ServoDlg.h                    |   20 
 SourceCode/Bond/DAQBridge/core/ConnEvents.h         |   40 
 SourceCode/Bond/DAQBridge/net/SocketComm.cpp        |  105 
 SourceCode/Bond/Servo/ProcessJob.h                  |   37 
 SourceCode/Bond/DAQBridge/proto/ProtocolCodec.h     |   49 
 SourceCode/Bond/Servo/CCjPageBase.h                 |   56 
 SourceCode/Bond/Servo/CControlJob.cpp               |   69 
 SourceCode/Bond/Servo/CGlass.cpp                    |  146 
 SourceCode/Bond/Servo/CCjPage3.h                    |   35 
 SourceCode/Bond/Servo/CControlJobManagerDlg.h       |   69 
 SourceCode/Bond/Servo/CCjPage1.h                    |   31 
 SourceCode/Bond/Servo/CControlJobManagerDlg.cpp     |  652 +++
 SourceCode/Bond/Servo/CLoadPort.cpp                 |    6 
 SourceCode/Bond/Servo/CPageGlassList.cpp            |  791 ++++
 SourceCode/Bond/DAQBridge/buffer/SampleBuffer.cpp   |  116 
 SourceCode/Bond/Servo/CMeasurement.cpp              |   27 
 SourceCode/Bond/Servo/CPageVarialbles.cpp           |    1 
 SourceCode/Bond/Servo/Servo.vcxproj.filters         |   79 
 SourceCode/Bond/Servo/TopToolbar.cpp                |    8 
 SourceCode/Bond/DAQBridge/core/CommBase.h           |   29 
 SourceCode/Bond/Servo/CEquipment.h                  |    8 
 SourceCode/Bond/Servo/CPageReport.cpp               |    1 
 Document/EO2860AVA-101工艺参数(4).xlsx                  |    0 
 SourceCode/Bond/DAQBridge/core/Collector.cpp        |  537 +++
 SourceCode/Bond/Servo/CParam.cpp                    |    7 
 SourceCode/Bond/Servo/CPagePortProperty.cpp         |    5 
 SourceCode/Bond/DAQBridge/buffer/BufferManager.h    |   72 
 SourceCode/Bond/Servo/Servo.vcxproj                 |   59 
 SourceCode/Bond/Servo/CCjPage1.cpp                  |   65 
 SourceCode/Bond/Servo/CCjPageBase.cpp               |  137 
 SourceCode/Bond/Servo/CControlJobDlg.cpp            |  297 +
 .gitignore                                          |    3 
 SourceCode/Bond/Servo/resource.h                    |    0 
 SourceCode/Bond/Servo/CServoUtilsTool.h             |    2 
 SourceCode/Bond/Servo/Servo.vcxproj.user            |    2 
 SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico  |    0 
 SourceCode/Bond/Servo/CCarrierSlotSelector.cpp      |  673 ++++
 SourceCode/Bond/Servo/CGlass.h                      |   64 
 SourceCode/Bond/Servo/Servo.cpp                     |    2 
 SourceCode/Bond/Servo/CExpandableListCtrl.cpp       |  135 
 SourceCode/Bond/Servo/PageLog.cpp                   |    2 
 SourceCode/Bond/Servo/Configuration.cpp             |   10 
 SourceCode/Bond/Servo/CMaster.h                     |   45 
 SourceCode/Bond/Servo/CCjPage2.cpp                  |  480 ++
 SourceCode/Bond/Servo/CPageGlassList.h              |   17 
 SourceCode/Bond/Servo/ClientListDlg.cpp             |  141 
 SourceCode/Bond/Servo/TopToolbar.h                  |    1 
 SourceCode/Bond/DAQBridge/buffer/BufferManager.cpp  |  128 
 SourceCode/Bond/Servo/CEquipment.cpp                |   43 
 SourceCode/Bond/Servo/Model.h                       |    1 
 SourceCode/Bond/Servo/GroupLabel.cpp                |  275 +
 SourceCode/Bond/Servo/CMaster.cpp                   |  611 ++
 SourceCode/Bond/DAQBridge/net/SocketComm.h          |   48 
 SourceCode/Bond/Servo/ToolUnits.h                   |    1 
 SourceCode/Bond/Servo/CPageCollectionEvent.cpp      |    1 
 SourceCode/Bond/Servo/CCarrierSlotGrid.h            |  195 +
 SourceCode/Bond/Servo/TerminalDisplayDlg.cpp        |    1 
 Document/EO2860AVA-101工艺参数(3).xlsx                  |    0 
 SourceCode/Bond/Servo/CCjPage2.h                    |   58 
 SourceCode/Bond/Servo/Model.cpp                     |   13 
 SourceCode/Bond/Servo/ServoDlg.cpp                  |  160 
 SourceCode/Bond/DAQBridge/DAQConfig.h               |    7 
 SourceCode/Bond/Servo/CCjPage3.cpp                  |  109 
 SourceCode/Bond/x64/Debug/EqsGraph.ini              |    2 
 SourceCode/Bond/Servo/CServoUtilsTool.cpp           |   77 
 SourceCode/Bond/DAQBridge/buffer/BufferRegistry.cpp |   35 
 SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico  |    0 
 SourceCode/Bond/Servo/CBonder.cpp                   |    4 
 94 files changed, 9,619 insertions(+), 460 deletions(-)

diff --git a/.gitignore b/.gitignore
index 38a88e4..e97c776 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,6 @@
 SourceCode/Bond/EAPSimulator/x64/
 Document/___CEID_________________.csv
 Document/______CEID__.csv
+SourceCode/Bond/x64/Debug/HsmsPassive.cache
+SourceCode/Bond/x64/Debug/MasterState.dat
+SourceCode/Bond/x64/Debug/Recipe/EQ10_Unit0.recipelist
diff --git "a/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0503\051.xlsx" "b/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0503\051.xlsx"
new file mode 100644
index 0000000..edd9542
--- /dev/null
+++ "b/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0503\051.xlsx"
Binary files differ
diff --git "a/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0504\051.xlsx" "b/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0504\051.xlsx"
new file mode 100644
index 0000000..4500fd0
--- /dev/null
+++ "b/Document/EO2860AVA-101\345\267\245\350\211\272\345\217\202\346\225\260\0504\051.xlsx"
Binary files differ
diff --git a/SourceCode/Bond/DAQBridge/DAQConfig.h b/SourceCode/Bond/DAQBridge/DAQConfig.h
new file mode 100644
index 0000000..27fff9e
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/DAQConfig.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <cstdint>
+namespace DAQCfg {
+    inline constexpr const char* Version = "1.0.1";
+    inline constexpr uint32_t KickIfNoVersionSec = 5;
+    inline constexpr uint32_t RecvLoopIntervalMs = 5;
+}
diff --git a/SourceCode/Bond/DAQBridge/buffer/BufferManager.cpp b/SourceCode/Bond/DAQBridge/buffer/BufferManager.cpp
new file mode 100644
index 0000000..7c0ec72
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/BufferManager.cpp
@@ -0,0 +1,128 @@
+// BufferManager.cpp
+#include "BufferManager.h"
+
+BufferManager::BufferManager(uint32_t id, std::string name, RetentionPolicy defaultPolicy)
+    : id_(id), name_(std::move(name)), defPolicy_(defaultPolicy) {}
+
+std::string BufferManager::name() const {
+    std::shared_lock lk(mtx_);
+    return name_;
+}
+void BufferManager::rename(const std::string& newName) {
+    std::unique_lock lk(mtx_);
+    name_ = newName;
+}
+
+void BufferManager::start() {
+    std::unique_lock lk(mtx_);
+    for (auto& kv : map_) kv.second->clear();
+    running_.store(true);
+}
+void BufferManager::stop() {
+    running_.store(false);
+}
+void BufferManager::clearAll() {
+    std::unique_lock lk(mtx_);
+    for (auto& kv : map_) kv.second->clear();
+}
+
+SampleBuffer& BufferManager::getOrCreate(uint32_t channelId) {
+    auto it = map_.find(channelId);
+    if (it != map_.end()) return *it->second;
+    auto buf = std::make_unique<SampleBuffer>(defPolicy_);
+    auto& ref = *buf;
+    map_[channelId] = std::move(buf);
+    return ref;
+}
+
+void BufferManager::push(uint32_t channelId, int64_t ts_ms, double v) {
+    if (!running_.load()) return; // 停止时丢弃新数据
+    std::unique_lock lk(mtx_);
+    getOrCreate(channelId).push(ts_ms, v);
+}
+
+std::vector<Sample> BufferManager::getSince(uint32_t channelId, int64_t tsExclusive, size_t maxCount) const {
+    std::shared_lock lk(mtx_);
+    auto it = map_.find(channelId);
+    if (it == map_.end()) return {};
+    return it->second->getSince(tsExclusive, maxCount);
+}
+std::vector<Sample> BufferManager::getRange(uint32_t channelId, int64_t from_ts, int64_t to_ts, size_t maxCount) const {
+    std::shared_lock lk(mtx_);
+    auto it = map_.find(channelId);
+    if (it == map_.end()) return {};
+    return it->second->getRange(from_ts, to_ts, maxCount);
+}
+
+void BufferManager::setDefaultPolicy(const RetentionPolicy& p, bool applyToExisting) {
+    std::unique_lock lk(mtx_);
+    defPolicy_ = p;
+    if (applyToExisting) {
+        for (auto& kv : map_) kv.second->setPolicy(p);
+    }
+}
+void BufferManager::setPolicy(uint32_t channelId, const RetentionPolicy& p) {
+    std::unique_lock lk(mtx_);
+    getOrCreate(channelId).setPolicy(p);
+}
+RetentionPolicy BufferManager::getPolicy(uint32_t channelId) const {
+    std::shared_lock lk(mtx_);
+    auto it = map_.find(channelId);
+    if (it == map_.end()) return defPolicy_;
+    return it->second->getPolicy();
+}
+
+std::vector<uint32_t> BufferManager::listChannels() const {
+    std::shared_lock lk(mtx_);
+    std::vector<uint32_t> ids; ids.reserve(map_.size());
+    for (auto& kv : map_) ids.push_back(kv.first);
+    return ids;
+}
+
+void BufferManager::setChannelName(uint32_t channelId, const std::string& name) {
+    std::unique_lock lk(mtx_);
+    channelNames_[channelId] = name;
+    // 强制创建对应的 SampleBuffer(可选)
+    (void)getOrCreate(channelId);
+}
+
+std::string BufferManager::getChannelName(uint32_t channelId) const {
+    std::shared_lock lk(mtx_);
+    auto it = channelNames_.find(channelId);
+    if (it != channelNames_.end() && !it->second.empty()) return it->second;
+    // 默认名
+    return "Ch-" + std::to_string(channelId);
+}
+
+std::vector<BufferManager::ChannelInfo> BufferManager::listChannelInfos() const {
+    std::shared_lock lk(mtx_);
+    std::vector<ChannelInfo> out;
+    out.reserve(map_.size());
+    // 以“已有缓冲器的通道”为基准列出;如需列出所有“命名过但尚未产生数据”的通道,也可遍历 channelNames_ 合并
+    for (auto& kv : map_) {
+        const uint32_t ch = kv.first;
+        auto it = channelNames_.find(ch);
+        out.push_back(ChannelInfo{ ch, (it != channelNames_.end() && !it->second.empty())
+                                        ? it->second
+                                        : ("Ch-" + std::to_string(ch)) });
+    }
+    return out;
+}
+
+BufferManager::ChannelStat BufferManager::getChannelStat(uint32_t channelId) const {
+    std::shared_lock lk(mtx_);
+    auto it = map_.find(channelId);
+    if (it == map_.end()) return ChannelStat{ channelId, 0, 0, 0 };
+    const auto& buf = *it->second;
+    return ChannelStat{ channelId, buf.size(), buf.earliestTs(), buf.latestTs() };
+}
+
+std::vector<BufferManager::ChannelStat> BufferManager::listChannelStats() const {
+    std::shared_lock lk(mtx_);
+    std::vector<ChannelStat> out; out.reserve(map_.size());
+    for (auto& kv : map_) {
+        const auto& buf = *kv.second;
+        out.push_back(ChannelStat{ kv.first, buf.size(), buf.earliestTs(), buf.latestTs() });
+    }
+    return out;
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/DAQBridge/buffer/BufferManager.h b/SourceCode/Bond/DAQBridge/buffer/BufferManager.h
new file mode 100644
index 0000000..564e16b
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/BufferManager.h
@@ -0,0 +1,72 @@
+#pragma once
+#include "SampleBuffer.h"
+#include <unordered_map>
+#include <memory>
+#include <shared_mutex>
+#include <string>
+#include <atomic>
+#include <vector>
+
+class BufferManager {
+public:
+    struct ChannelStat {
+        uint32_t id;
+        size_t   size;
+        int64_t  earliestTs;
+        int64_t  latestTs;
+    };
+
+public:
+    BufferManager(uint32_t id, std::string name, RetentionPolicy defaultPolicy = {});
+
+    // 标识
+    uint32_t id() const { return id_; }
+    std::string name() const;
+    void rename(const std::string& newName);
+
+    // 采集控制
+    void start();
+    void stop();
+    bool isRunning() const { return running_.load(); }
+    void clearAll();
+
+    // 数据写读
+    void push(uint32_t channelId, int64_t ts_ms, double v);
+    std::vector<Sample> getSince(uint32_t channelId, int64_t tsExclusive, size_t maxCount = 4096) const;
+    std::vector<Sample> getRange(uint32_t channelId, int64_t from_ts, int64_t to_ts, size_t maxCount = 4096) const;
+
+    // 策略
+    void setDefaultPolicy(const RetentionPolicy& p, bool applyToExisting = false);
+    void setPolicy(uint32_t channelId, const RetentionPolicy& p);
+    RetentionPolicy getPolicy(uint32_t channelId) const;
+
+    // 通道管理
+    std::vector<uint32_t> listChannels() const;
+
+    // ===== 新增:通道(曲线)名称 =====
+    void setChannelName(uint32_t channelId, const std::string& name); // 设置/更新
+    std::string getChannelName(uint32_t channelId) const;             // 若未设置,返回 "Ch-<id>"
+    struct ChannelInfo { uint32_t id; std::string name; };
+    std::vector<ChannelInfo> listChannelInfos() const;                // 方便 UI 列表
+
+    std::vector<ChannelStat> listChannelStats() const;
+    // 单个通道
+    ChannelStat getChannelStat(uint32_t channelId) const;
+private:
+    SampleBuffer& getOrCreate(uint32_t channelId);
+
+    mutable std::shared_mutex mtx_;
+    std::unordered_map<uint32_t, std::unique_ptr<SampleBuffer>> map_;
+
+    // ===== 新增:通道名称表 =====
+    std::unordered_map<uint32_t, std::string> channelNames_;
+
+    uint32_t id_;
+    std::string name_;
+    RetentionPolicy defPolicy_;
+    std::atomic<bool> running_{ false };
+
+
+
+
+};
diff --git a/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.cpp b/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.cpp
new file mode 100644
index 0000000..19e76ef
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.cpp
@@ -0,0 +1,35 @@
+// BufferRegistry.cpp
+#include "BufferRegistry.h"
+
+BufferManager& BufferRegistry::getOrCreate(uint32_t managerId, const std::string& name, const RetentionPolicy& defPolicy) {
+    std::unique_lock lk(mtx_);
+    auto it = managers_.find(managerId);
+    if (it != managers_.end()) return *it->second;
+    auto bm = std::make_unique<BufferManager>(managerId, name, defPolicy);
+    auto& ref = *bm;
+    managers_[managerId] = std::move(bm);
+    return ref;
+}
+
+std::vector<uint32_t> BufferRegistry::listManagers() const {
+    std::shared_lock lk(mtx_);
+    std::vector<uint32_t> ids; ids.reserve(managers_.size());
+    for (auto& kv : managers_) ids.push_back(kv.first);
+    return ids;
+}
+
+BufferManager* BufferRegistry::find(uint32_t managerId) {
+    std::shared_lock lk(mtx_);
+    auto it = managers_.find(managerId);
+    return (it == managers_.end()) ? nullptr : it->second.get();
+}
+const BufferManager* BufferRegistry::find(uint32_t managerId) const {
+    std::shared_lock lk(mtx_);
+    auto it = managers_.find(managerId);
+    return (it == managers_.end()) ? nullptr : it->second.get();
+}
+
+void BufferRegistry::remove(uint32_t managerId) {
+    std::unique_lock lk(mtx_);
+    managers_.erase(managerId);
+}
diff --git a/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.h b/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.h
new file mode 100644
index 0000000..2cc44d0
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/BufferRegistry.h
@@ -0,0 +1,24 @@
+// BufferRegistry.h
+#pragma once
+#include "BufferManager.h"
+#include <unordered_map>
+#include <memory>
+#include <shared_mutex>
+
+class BufferRegistry {
+public:
+    // 获取/创建某台机器的 BufferManager
+    BufferManager& getOrCreate(uint32_t managerId, const std::string& name, const RetentionPolicy& defPolicy = {});
+
+    // 查询
+    std::vector<uint32_t> listManagers() const;
+    BufferManager* find(uint32_t managerId);
+    const BufferManager* find(uint32_t managerId) const;
+
+    // 移除一个管理器(可选)
+    void remove(uint32_t managerId);
+
+private:
+    mutable std::shared_mutex mtx_;
+    std::unordered_map<uint32_t, std::unique_ptr<BufferManager>> managers_;
+};
diff --git a/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.cpp b/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.cpp
new file mode 100644
index 0000000..be7145e
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.cpp
@@ -0,0 +1,116 @@
+// SampleBuffer.cpp
+#include "SampleBuffer.h"
+#include <algorithm>
+
+void SampleBuffer::push(int64_t ts_ms, double v) {
+    std::unique_lock lk(mtx_);
+    if (!data_.empty() && ts_ms < data_.back().ts_ms) {
+        ts_ms = data_.back().ts_ms; // 简单纠正为非递减;严格场景可改为丢弃/插入排序
+    }
+    data_.push_back({ ts_ms, v });
+    pruneUnlocked(ts_ms); // 用当前写入时间作为参考时间
+}
+
+std::vector<Sample> SampleBuffer::getSince(int64_t tsExclusive, size_t maxCount) const {
+    std::shared_lock lk(mtx_);
+    std::vector<Sample> out;
+    if (data_.empty()) return out;
+    const size_t start = lowerBoundIndex(tsExclusive);
+    if (start >= data_.size()) return out;
+    const size_t n = std::min(maxCount, data_.size() - start);
+    out.reserve(n);
+    for (size_t i = 0; i < n; ++i) out.push_back(data_[start + i]);
+    return out;
+}
+
+std::vector<Sample> SampleBuffer::getRange(int64_t from_ts, int64_t to_ts, size_t maxCount) const {
+    if (to_ts < from_ts) return {};
+    std::shared_lock lk(mtx_);
+    std::vector<Sample> out;
+    if (data_.empty()) return out;
+    const size_t L = lowerBoundInclusive(from_ts);
+    const size_t R = upperBoundInclusive(to_ts);
+    if (L >= R) return out;
+    const size_t n = std::min(maxCount, R - L);
+    out.reserve(n);
+    for (size_t i = 0; i < n; ++i) out.push_back(data_[L + i]);
+    return out;
+}
+
+size_t SampleBuffer::size() const { std::shared_lock lk(mtx_); return data_.size(); }
+bool   SampleBuffer::empty() const { std::shared_lock lk(mtx_); return data_.empty(); }
+int64_t SampleBuffer::latestTs() const {
+    std::shared_lock lk(mtx_);
+    return data_.empty() ? 0 : data_.back().ts_ms;
+}
+void SampleBuffer::clear() {
+    std::unique_lock lk(mtx_);
+    data_.clear();
+}
+
+void SampleBuffer::setPolicy(const RetentionPolicy& p) {
+    std::unique_lock lk(mtx_);
+    policy_ = p;
+    // 立即按新策略清一次(以“当前最新 ts”为参考)
+    pruneUnlocked(data_.empty() ? 0 : data_.back().ts_ms);
+}
+RetentionPolicy SampleBuffer::getPolicy() const {
+    std::shared_lock lk(mtx_);
+    return policy_;
+}
+
+void SampleBuffer::pruneUnlocked(int64_t ref_now_ms) {
+    switch (policy_.mode) {
+    case RetainMode::ByCount:
+        if (policy_.maxSamples > 0) {
+            while (data_.size() > policy_.maxSamples) data_.pop_front();
+        }
+        break;
+    case RetainMode::ByRollingAge: {
+        if (policy_.maxAge.count() > 0) {
+            const int64_t cutoff = ref_now_ms - policy_.maxAge.count();
+            while (!data_.empty() && data_.front().ts_ms < cutoff) data_.pop_front();
+        }
+        break;
+    }
+    case RetainMode::ByAbsoluteRange: {
+        const bool valid = (policy_.absTo >= policy_.absFrom);
+        if (valid) {
+            // 丢弃窗口之外的数据(两端都裁)
+            while (!data_.empty() && data_.front().ts_ms < policy_.absFrom) data_.pop_front();
+            while (!data_.empty() && data_.back().ts_ms > policy_.absTo)   data_.pop_back();
+        }
+        break;
+    }
+    }
+}
+
+size_t SampleBuffer::lowerBoundIndex(int64_t tsExclusive) const {
+    size_t L = 0, R = data_.size();
+    while (L < R) {
+        size_t m = (L + R) >> 1;
+        if (data_[m].ts_ms <= tsExclusive) L = m + 1; else R = m;
+    }
+    return L; // 第一个 > tsExclusive
+}
+size_t SampleBuffer::lowerBoundInclusive(int64_t tsInclusive) const {
+    size_t L = 0, R = data_.size();
+    while (L < R) {
+        size_t m = (L + R) >> 1;
+        if (data_[m].ts_ms < tsInclusive) L = m + 1; else R = m;
+    }
+    return L; // 第一个 >= tsInclusive
+}
+size_t SampleBuffer::upperBoundInclusive(int64_t tsInclusive) const {
+    size_t L = 0, R = data_.size();
+    while (L < R) {
+        size_t m = (L + R) >> 1;
+        if (data_[m].ts_ms <= tsInclusive) L = m + 1; else R = m;
+    }
+    return L; // 最后一个 <= tsInclusive 的下一个位置
+}
+
+int64_t SampleBuffer::earliestTs() const {
+    std::shared_lock lk(mtx_);
+    return data_.empty() ? 0 : data_.front().ts_ms;
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.h b/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.h
new file mode 100644
index 0000000..12d44f8
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/buffer/SampleBuffer.h
@@ -0,0 +1,57 @@
+// SampleBuffer.h
+#pragma once
+#include "../core/DataTypes.h"
+#include <deque>
+#include <vector>
+#include <shared_mutex>
+#include <chrono>
+
+enum class RetainMode {
+    ByCount,        // 按样本数上限
+    ByRollingAge,   // 按滚动时间窗口(maxAge)
+    ByAbsoluteRange // 按绝对时间段 [absFrom, absTo]
+};
+
+struct RetentionPolicy {
+    RetainMode mode = RetainMode::ByCount;
+    size_t maxSamples = 100000;                         // ByCount 用
+    std::chrono::milliseconds maxAge{ std::chrono::hours(1) }; // ByRollingAge 用
+    int64_t absFrom = 0;                                // ByAbsoluteRange 用
+    int64_t absTo = 0;                                // ByAbsoluteRange 用(absTo>=absFrom 有效)
+};
+
+class SampleBuffer {
+public:
+    explicit SampleBuffer(RetentionPolicy policy = {}) : policy_(policy) {}
+
+    // 写:按时间戳递增推入(乱序会被简单纠正为非递减)
+    void push(int64_t ts_ms, double v);
+
+    // 读:按“tsExclusive 之后”的新数据
+    std::vector<Sample> getSince(int64_t tsExclusive, size_t maxCount = 4096) const;
+
+    // 读:按区间 [from, to](包含边界)
+    std::vector<Sample> getRange(int64_t from_ts, int64_t to_ts, size_t maxCount = 4096) const;
+
+    // 查询 / 维护
+    size_t size() const;
+    bool   empty() const;
+    int64_t latestTs() const;
+    void clear();
+
+    // 配置
+    void setPolicy(const RetentionPolicy& p);
+    RetentionPolicy getPolicy() const;
+
+    int64_t earliestTs() const;  // 无数据时返回 0
+
+private:
+    void pruneUnlocked(int64_t ref_now_ms);       // 按策略清理
+    size_t lowerBoundIndex(int64_t tsExclusive) const; // 二分:第一个 > tsExclusive
+    size_t lowerBoundInclusive(int64_t tsInclusive) const; // 第一个 >= tsInclusive
+    size_t upperBoundInclusive(int64_t tsInclusive) const; // 最后一个 <= tsInclusive 的下一个位置
+
+    mutable std::shared_mutex mtx_;
+    std::deque<Sample> data_;
+    RetentionPolicy policy_;
+};
diff --git a/SourceCode/Bond/DAQBridge/core/Collector.cpp b/SourceCode/Bond/DAQBridge/core/Collector.cpp
new file mode 100644
index 0000000..7cb2d3d
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/Collector.cpp
@@ -0,0 +1,537 @@
+#include "Collector.h"
+#include <iostream>
+#include <chrono>
+
+#include "../DAQConfig.h"
+#include "../core/ConnEvents.h"
+using namespace DAQEvt;
+
+// 协议编解码
+#include "../proto/Protocol.h"
+#include "../proto/ProtocolCodec.h"
+
+using namespace Proto;
+
+Collector::Collector() {}
+
+void Collector::startLoop(uint32_t intervalMs) {
+    if (running.load()) return;
+    running.store(true);
+    worker = std::thread([this, intervalMs]() {
+        while (running.load()) {
+            this->poll();
+            std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
+        }
+        });
+}
+
+void Collector::stopLoop() {
+    if (!running.load()) return;
+    running.store(false);
+    if (worker.joinable()) worker.join();
+}
+
+Collector::~Collector() {
+    stopLoop();
+    disconnect();
+}
+
+std::vector<Collector::ClientSummary> Collector::getClientList() {
+    std::lock_guard<std::mutex> lk(mClients);
+    std::vector<ClientSummary> out;
+    out.reserve(clients.size());
+    for (const auto& c : clients) {
+        out.push_back(ClientSummary{ c.ip, c.port, c.versionOk, c.sock });
+    }
+    return out;
+}
+
+void Collector::createServer(uint16_t port) {
+    if (socketComm.createServerSocket(port)) {
+        if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::ServerListening),
+            "Collector server listening on port " + std::to_string(port));
+        onConnectionEstablished();
+    }
+}
+
+void Collector::disconnect() {
+    {
+        std::lock_guard<std::mutex> lk(mClients);
+        for (auto& c : clients) {
+            socketComm.closeClient(c.sock);
+            if (cbClientEvent) cbClientEvent(c.ip, c.port, /*connected*/false);
+        }
+        clients.clear();
+    }
+    socketComm.closeSocket();
+    if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::ServerStopped), "Collector server stopped");
+    onConnectionLost();
+}
+
+void Collector::poll() {
+    // 1) 接新客户端
+    while (true) {
+        SOCKET cs; std::string ip; uint16_t port;
+        if (!socketComm.acceptOne(cs, ip, port)) break;
+
+        ClientInfo ci;
+        ci.sock = cs; ci.ip = ip; ci.port = port;
+        ci.tsConnected = std::chrono::steady_clock::now();
+
+        {
+            std::lock_guard<std::mutex> lk(mClients);
+            clients.push_back(ci);
+        }
+
+        if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::ClientAccepted),
+            "Client connected: " + ip + ":" + std::to_string(port));
+        if (cbClientEvent) cbClientEvent(ip, port, /*connected*/true);
+    }
+
+    // 2) 轮询各客户端
+    auto now = std::chrono::steady_clock::now();
+    std::lock_guard<std::mutex> lk(mClients);
+    for (size_t i = 0; i < clients.size(); ) {
+        auto& c = clients[i];
+
+        // 2a) 版本握手超时
+        if (!c.versionOk) {
+            auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - c.tsConnected).count();
+            if (elapsed >= DAQCfg::KickIfNoVersionSec) {
+                if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::VersionTimeoutKick),
+                    "Kick client (no version within 5s): " + c.ip + ":" + std::to_string(c.port));
+                socketComm.closeClient(c.sock);
+                if (cbClientEvent) cbClientEvent(c.ip, c.port, /*connected*/false);
+                clients.erase(clients.begin() + i);
+                continue;
+            }
+        }
+
+        // 2b) 收数据
+        std::vector<uint8_t> buf;
+        bool peerClosed = false;
+        if (socketComm.recvFrom(c.sock, buf, peerClosed)) {
+            if (!buf.empty()) {
+                if (rawDumpEnabled && cbRaw) cbRaw(buf);
+
+                // 帧组装
+                c.fr.push(buf);
+                std::vector<uint8_t> frame;
+                while (c.fr.nextFrame(frame)) {
+                    if (rawDumpEnabled && cbRaw) cbRaw(frame);
+                    handleClientData(c, frame);
+                }
+            }
+        }
+        else if (peerClosed) {
+            // 对端主动断开
+            if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::ClientDisconnected),
+                "Client disconnected: " + c.ip + ":" + std::to_string(c.port));
+            socketComm.closeClient(c.sock);
+            if (cbClientEvent) cbClientEvent(c.ip, c.port, /*connected*/false);
+            clients.erase(clients.begin() + i);
+            continue;
+        }
+
+        ++i;
+    }
+}
+
+void Collector::sendSampleData(double sample) {
+    (void)sample;
+#ifdef _DEBUG
+    if (cbStatus) cbStatus(0, "sendSampleData() no-op.");
+#endif
+}
+
+void Collector::sendWindowData(const std::vector<std::string>& fields) {
+    (void)fields;
+#ifdef _DEBUG
+    if (cbStatus) cbStatus(0, "sendWindowData() no-op.");
+#endif
+}
+
+void Collector::onConnectionEstablished() {
+    std::cout << "[Collector] ready.\n";
+}
+void Collector::onConnectionLost() {
+    std::cout << "[Collector] stopped.\n";
+}
+
+// 兼容旧门禁函数
+bool Collector::isVersionRequest(const std::vector<uint8_t>& data) const {
+    if (data.size() < 4 + 4 + 2 + 2 + 1) return false;
+    if (!(data[0] == 0x11 && data[1] == 0x88 && data[2] == 0x11 && data[3] == 0x88)) return false;
+    size_t cmdPos = 4 + 4 + 2;
+    uint16_t cmd = (uint16_t(data[cmdPos]) << 8) | data[cmdPos + 1];
+    return (cmd == CMD_REQ_VERSION);
+}
+
+// ========== BufferRegistry 直通 ==========
+BufferManager& Collector::registryAddMachine(uint32_t machineId, const std::string& name,
+    const RetentionPolicy& defPolicy) {
+    return registry_.getOrCreate(machineId, name, defPolicy);
+}
+BufferManager* Collector::registryFind(uint32_t machineId) { return registry_.find(machineId); }
+const BufferManager* Collector::registryFind(uint32_t machineId) const { return registry_.find(machineId); }
+std::vector<uint32_t> Collector::registryListMachines() const { return registry_.listManagers(); }
+
+void Collector::buffersPush(uint32_t machineId, uint32_t channelId, int64_t ts_ms, double v) {
+    auto& bm = registry_.getOrCreate(machineId, "Machine-" + std::to_string(machineId));
+    bm.push(channelId, ts_ms, v); // 未 start() 时将被内部忽略
+}
+
+std::vector<Sample> Collector::buffersGetSince(uint32_t machineId, uint32_t channelId,
+    int64_t tsExclusive, size_t maxCount) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return {};
+    return bm->getSince(channelId, tsExclusive, maxCount);
+}
+std::vector<Sample> Collector::buffersGetRange(uint32_t machineId, uint32_t channelId,
+    int64_t from_ts, int64_t to_ts, size_t maxCount) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return {};
+    return bm->getRange(channelId, from_ts, to_ts, maxCount);
+}
+
+void Collector::buffersStart(uint32_t machineId) {
+    auto& bm = registry_.getOrCreate(machineId, "Machine-" + std::to_string(machineId));
+    bm.start(); // 清空 + 允许写
+}
+void Collector::buffersStop(uint32_t machineId) {
+    if (auto* bm = registry_.find(machineId)) bm->stop();
+}
+void Collector::buffersClear(uint32_t machineId) {
+    if (auto* bm = registry_.find(machineId)) bm->clearAll();
+}
+bool Collector::buffersIsRunning(uint32_t machineId) const {
+    auto* bm = registry_.find(machineId);
+    return bm ? bm->isRunning() : false;
+}
+
+void Collector::buffersSetDefaultPolicy(uint32_t machineId, const RetentionPolicy& p, bool applyExisting) {
+    auto& bm = registry_.getOrCreate(machineId, "Machine-" + std::to_string(machineId));
+    bm.setDefaultPolicy(p, applyExisting);
+}
+void Collector::buffersSetPolicy(uint32_t machineId, uint32_t channelId, const RetentionPolicy& p) {
+    auto& bm = registry_.getOrCreate(machineId, "Machine-" + std::to_string(machineId));
+    bm.setPolicy(channelId, p);
+}
+void Collector::buffersSetChannelName(uint32_t machineId, uint32_t channelId, const std::string& name) {
+    auto& bm = registry_.getOrCreate(machineId, "Machine-" + std::to_string(machineId));
+    bm.setChannelName(channelId, name);
+}
+std::string Collector::buffersGetChannelName(uint32_t machineId, uint32_t channelId) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return {};
+    return bm->getChannelName(channelId);
+}
+std::vector<BufferManager::ChannelInfo> Collector::buffersListChannelInfos(uint32_t machineId) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return {};
+    return bm->listChannelInfos();
+}
+std::vector<BufferManager::ChannelStat> Collector::buffersListChannelStats(uint32_t machineId) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return {};
+    return bm->listChannelStats();
+}
+BufferManager::ChannelStat Collector::buffersGetChannelStat(uint32_t machineId, uint32_t channelId) const {
+    auto* bm = registry_.find(machineId);
+    if (!bm) return BufferManager::ChannelStat{ channelId, 0, 0, 0 };
+    return bm->getChannelStat(channelId);
+}
+
+// ====== 批次管理 ======
+// 新实现:根据 expectedDurationMs 计算 expectedEndTs
+void Collector::batchStart(uint32_t machineId,
+    const std::string& batchId,
+    uint64_t expectedDurationMs)
+{
+    const uint64_t startTs = nowMs();
+    const uint64_t expectedEndTs = (expectedDurationMs > 0) ? (startTs + expectedDurationMs) : 0;
+
+    {
+        std::lock_guard<std::mutex> lk(mBatches);
+        auto& br = batches_[machineId];
+        br.state = BatchState::Active;
+        br.activeBatchId = batchId;
+        br.activeStartTs = startTs;
+        br.expectedEndTs = expectedEndTs;
+    }
+
+    // 批次开始:清空并开始采集
+    buffersStart(machineId);
+
+    if (cbStatus) cbStatus(0, "batchStart: machine " + std::to_string(machineId) +
+        " batch=" + batchId +
+        " durMs=" + std::to_string(expectedDurationMs));
+}
+
+// 兼容旧签名:忽略 startTs;把 expectedEndTs 当“时长”
+void Collector::batchStart(uint32_t machineId,
+    const std::string& batchId,
+    uint64_t /*startTs_ignored*/,
+    uint64_t expectedEndTs_orDuration)
+{
+    // 若调用方误传了绝对的“结束时间戳”,这里会被当作“时长”;
+    // 如需严格区分,你也可以加一个阈值判断(比如大于 10^12 认为是绝对时间戳)
+    // 然后转换:duration = max(0, endTs - now)
+
+    uint64_t durationMs = expectedEndTs_orDuration;
+
+    // 可选:做个简单的“像绝对时间戳”的判断并自动转为时长
+    // 以毫秒时间戳阈值举例(~2001-09-09):1e12
+    if (expectedEndTs_orDuration > 1000000000000ULL) {
+        uint64_t now = nowMs();
+        if (expectedEndTs_orDuration > now)
+            durationMs = expectedEndTs_orDuration - now;
+        else
+            durationMs = 0; // 已过期,当未知处理
+    }
+
+    batchStart(machineId, batchId, durationMs);
+}
+
+void Collector::batchStop(uint32_t machineId, uint64_t /*endTs*/) {
+    {
+        std::lock_guard<std::mutex> lk(mBatches);
+        auto it = batches_.find(machineId);
+        if (it != batches_.end()) {
+            it->second.state = BatchState::Idle;
+            it->second.activeBatchId.clear();
+            it->second.activeStartTs = 0;
+            it->second.expectedEndTs = 0;
+        }
+        else {
+            batches_[machineId] = BatchRec{}; // 生成一条 Idle
+        }
+    }
+
+    // 停止采集,但不清数据
+    buffersStop(machineId);
+
+    if (cbStatus) cbStatus(0, "batchStop: machine " + std::to_string(machineId));
+}
+
+Collector::BatchRec Collector::getBatch(uint32_t machineId) const {
+    std::lock_guard<std::mutex> lk(mBatches);
+    auto it = batches_.find(machineId);
+    if (it != batches_.end()) return it->second;
+    return BatchRec{}; // 默认 Idle
+}
+
+// ========== 业务分发 ==========
+void Collector::handleClientData(ClientInfo& c, const std::vector<uint8_t>& data) {
+    // 版本校验
+    if (!c.versionOk) {
+        ReqVersion vreq{};
+        if (decodeRequestVersion(data, vreq)) {
+            c.versionOk = true;
+            if (cbStatus) cbStatus(static_cast<int>(DAQEvt::ConnCode::VersionOk),
+                "Client " + c.ip + ":" + std::to_string(c.port) + " version OK");
+
+            RspVersion vrst;
+            vrst.dataId = vreq.dataId;         // 回显
+            vrst.version = DAQCfg::Version;    // 集中配置
+            auto rsp = encodeResponseVersion(vrst);
+            socketComm.sendTo(c.sock, rsp);
+        }
+        return; // 未通过版本校验,其它忽略
+    }
+
+    // 通过版本校验后:按 cmd 分发
+    const uint16_t cmd = peek_cmd(data);
+
+    // 0x0101 —— 单通道增量拉取
+    if (cmd == CMD_REQ_SINCE) {
+        ReqSince req;
+        if (!decodeRequestSince(data, req)) return;
+
+        auto vec = buffersGetSince(req.machineId, req.channelId,
+            static_cast<int64_t>(req.sinceTsExclusive), req.maxCount);
+
+        uint64_t lastTsSent = req.sinceTsExclusive;
+        if (!vec.empty()) lastTsSent = static_cast<uint64_t>(vec.back().ts_ms);
+
+        auto stat = buffersGetChannelStat(req.machineId, req.channelId);
+        uint8_t more = (stat.latestTs > static_cast<int64_t>(lastTsSent)) ? 1 : 0;
+
+        RspSince rsp;
+        rsp.dataId = req.dataId; // 回显
+        rsp.machineId = req.machineId;
+        rsp.channelId = req.channelId;
+        rsp.lastTsSent = lastTsSent;
+        rsp.more = more;
+        rsp.samples.reserve(vec.size());
+        for (auto& s : vec) rsp.samples.push_back({ static_cast<uint64_t>(s.ts_ms), s.value });
+
+        auto pkt = encodeResponseSince(rsp);
+        socketComm.sendTo(c.sock, pkt);
+        return;
+    }
+
+    // 0x0103 —— 统计/通道列表
+    if (cmd == CMD_REQ_STATS) {
+        ReqStats req;
+        if (!decodeRequestStats(data, req)) return;
+
+        auto* bm = registry_.find(req.machineId);
+        if (!bm) {
+            // 示例:可回错误帧
+            // sendErrorTo(c, req.dataId, CMD_REQ_STATS, req.machineId, ErrCode::NoActiveBatch, "machine not found");
+            return;
+        }
+
+        RspStats rsp;
+        rsp.dataId = req.dataId;
+        rsp.machineId = req.machineId;
+
+        auto stats = bm->listChannelStats();
+        rsp.channels.reserve(stats.size());
+        for (auto& s : stats) {
+            ChannelStatInfo ci;
+            ci.channelId = s.id;
+            ci.earliestTs = static_cast<uint64_t>(s.earliestTs);
+            ci.latestTs = static_cast<uint64_t>(s.latestTs);
+            ci.size = static_cast<uint32_t>(s.size);
+            ci.name = bm->getChannelName(s.id); // UTF-8
+            rsp.channels.push_back(std::move(ci));
+        }
+
+        auto pkt = encodeResponseStats(rsp);
+        socketComm.sendTo(c.sock, pkt);
+        return;
+    }
+
+    // 0x0104 —— 机台列表
+    if (cmd == CMD_REQ_MACHINES) {
+        ReqMachines req;
+        if (!decodeRequestMachines(data, req)) return;
+
+        RspMachines rsp;
+        rsp.dataId = req.dataId;
+
+        auto mids = registry_.listManagers();
+        rsp.machines.reserve(mids.size());
+        for (auto id : mids) {
+            auto* bm = registry_.find(id);
+            if (!bm) continue;
+            rsp.machines.push_back(MachineInfo{ id, bm->name() });
+        }
+
+        auto pkt = encodeResponseMachines(rsp);
+        socketComm.sendTo(c.sock, pkt);
+        return;
+    }
+
+    // 0x0105 —— 整机多通道增量拉取
+    if (cmd == CMD_REQ_SINCE_ALL) {
+        ReqSinceAll req;
+        if (!decodeRequestSinceAll(data, req)) return;
+
+        RspSinceAll rsp;
+        rsp.dataId = req.dataId;      // 回显
+        rsp.machineId = req.machineId;
+        rsp.moreAny = 0;
+
+        auto* bm = registry_.find(req.machineId);
+        if (!bm) {
+            auto pkt = encodeResponseSinceAll(rsp);
+            socketComm.sendTo(c.sock, pkt);
+            return;
+        }
+
+        // 避免超过 2 字节长度:正文 ~60KB
+        constexpr size_t kMaxBody = 60 * 1024;
+
+        rsp.blocks.clear();
+        auto stats = bm->listChannelStats();
+        for (auto& s : stats) {
+            auto vec = bm->getSince(s.id, static_cast<int64_t>(req.sinceTsExclusive), req.maxPerChannel);
+            if (vec.empty()) continue;
+
+            ChannelBlock blk;
+            blk.channelId = s.id;
+            blk.lastTsSent = static_cast<uint64_t>(vec.back().ts_ms);
+
+            auto st = bm->getChannelStat(s.id);
+            blk.more = (st.latestTs > static_cast<int64_t>(blk.lastTsSent)) ? 1 : 0;
+
+            blk.samples.reserve(vec.size());
+            for (auto& sm : vec) {
+                blk.samples.push_back({ static_cast<uint64_t>(sm.ts_ms), sm.value });
+            }
+
+            rsp.blocks.push_back(std::move(blk));
+            auto tentative = encodeResponseSinceAll(rsp);
+            if (tentative.size() > (4 + 4 + 2) + kMaxBody + 1) {
+                // 超体积:把最后一块拿出来,先发前面的
+                auto last = std::move(rsp.blocks.back());
+                rsp.blocks.pop_back();
+
+                auto pkt = encodeResponseSinceAll(rsp);
+                socketComm.sendTo(c.sock, pkt);
+
+                rsp.blocks.clear();
+                rsp.blocks.push_back(std::move(last));
+            }
+        }
+
+        rsp.moreAny = 0;
+        for (const auto& b : rsp.blocks) {
+            if (b.more) { rsp.moreAny = 1; break; }
+        }
+
+        auto pkt = encodeResponseSinceAll(rsp);
+        socketComm.sendTo(c.sock, pkt);
+        return;
+    }
+
+    // 0x0120 —— 批次信息
+    if (cmd == CMD_REQ_BATCH_INFO) {
+        ReqBatchInfo req;
+        if (!decodeRequestBatchInfo(data, req)) return;
+
+        RspBatchInfo rsp;
+        rsp.dataId = req.dataId;
+        rsp.machineId = req.machineId;
+
+        const auto br = getBatch(req.machineId);
+        rsp.state = br.state;
+        rsp.activeBatchId = br.activeBatchId;
+        rsp.activeStartTs = br.activeStartTs;
+        rsp.expectedEndTs = br.expectedEndTs;
+
+        auto pkt = encodeResponseBatchInfo(rsp);
+        socketComm.sendTo(c.sock, pkt);
+        return;
+    }
+
+    // 其它指令:按需扩展
+}
+
+// 统一错误回包
+void Collector::sendErrorTo(ClientInfo& c,
+    uint32_t dataId,
+    uint16_t refCmd,
+    uint32_t machineId,
+    ErrCode code,
+    const std::string& message)
+{
+    RspError er;
+    er.dataId = dataId;
+    er.refCmd = refCmd;
+    er.machineId = machineId;
+    er.code = code;
+    er.message = message;
+
+    auto pkt = encodeResponseError(er);
+    socketComm.sendTo(c.sock, pkt);
+}
+
+// 工具:当前 epoch 毫秒
+uint64_t Collector::nowMs() {
+    using namespace std::chrono;
+    return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
+}
diff --git a/SourceCode/Bond/DAQBridge/core/Collector.h b/SourceCode/Bond/DAQBridge/core/Collector.h
new file mode 100644
index 0000000..b595905
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/Collector.h
@@ -0,0 +1,163 @@
+#pragma once
+#ifndef COLLECTOR_H
+#define COLLECTOR_H
+
+#include "CommBase.h"
+#include "../net/SocketComm.h"
+
+#include <functional>
+#include <chrono>
+#include <thread>
+#include <atomic>
+#include <mutex>
+#include <vector>
+#include <string>
+#include <unordered_map>
+
+#include "../buffer/BufferRegistry.h"
+#include "../net/FrameAssembler.h"
+
+// 协议常量/结构(必须包含,供本头文件使用 Proto:: 类型)
+#include "../proto/Protocol.h"
+
+class Collector : public CommBase {
+public:
+    Collector();
+    ~Collector();
+
+    // ===== CommBase 接口 =====
+    void sendSampleData(double sample) override;
+    void sendWindowData(const std::vector<std::string>& dataFields) override;
+
+    void connectServer(const std::string& /*ip*/, uint16_t /*port*/) override {} // 采集端不作为客户端
+    void createServer(uint16_t port) override;
+    void disconnect() override;
+
+    void onConnectionEstablished() override;
+    void onConnectionLost() override;
+
+    // 连接/原始数据回调(UI)
+    void setConnectionStatusCallback(std::function<void(int, std::string)> cb) override { cbStatus = std::move(cb); }
+    void setRawDataCallback(std::function<void(const std::vector<uint8_t>&)> cb) override { cbRaw = std::move(cb); }
+    void setRawDumpEnabled(bool enabled) override { rawDumpEnabled = enabled; }
+
+    // 后台轮询(不阻塞 UI)
+    void startLoop(uint32_t intervalMs = 10);
+    void stopLoop();
+    void poll();
+
+    // 客户端进入/断开事件
+    void setClientEventCallback(std::function<void(const std::string& ip, uint16_t port, bool connected)> cb) {
+        cbClientEvent = std::move(cb);
+    }
+
+    // 客户端快照
+    struct ClientSummary {
+        std::string ip;
+        uint16_t    port = 0;
+        bool        versionOk = false;
+        SOCKET      sock = INVALID_SOCKET;
+    };
+    std::vector<ClientSummary> getClientList();
+
+    // ===== 缓冲/通道相关 =====
+    void        buffersSetChannelName(uint32_t machineId, uint32_t channelId, const std::string& name);
+    std::string buffersGetChannelName(uint32_t machineId, uint32_t channelId) const;
+    std::vector<BufferManager::ChannelInfo> buffersListChannelInfos(uint32_t machineId) const;
+
+    BufferManager& registryAddMachine(uint32_t machineId, const std::string& name,
+        const RetentionPolicy& defPolicy = {});
+    BufferManager* registryFind(uint32_t machineId);
+    const BufferManager* registryFind(uint32_t machineId) const;
+    std::vector<uint32_t> registryListMachines() const;
+
+    void buffersPush(uint32_t machineId, uint32_t channelId, int64_t ts_ms, double v);
+
+    std::vector<Sample> buffersGetSince(uint32_t machineId, uint32_t channelId,
+        int64_t tsExclusive, size_t maxCount = 4096) const;
+    std::vector<Sample> buffersGetRange(uint32_t machineId, uint32_t channelId,
+        int64_t from_ts, int64_t to_ts, size_t maxCount = 4096) const;
+
+    void buffersStart(uint32_t machineId);   // 清空该机历史并开始
+    void buffersStop(uint32_t machineId);    // 暂停(push 忽略)
+    void buffersClear(uint32_t machineId);   // 清空历史,不改 running 状态
+    bool buffersIsRunning(uint32_t machineId) const;
+
+    void buffersSetDefaultPolicy(uint32_t machineId, const RetentionPolicy& p, bool applyExisting = true);
+    void buffersSetPolicy(uint32_t machineId, uint32_t channelId, const RetentionPolicy& p);
+
+    std::vector<BufferManager::ChannelStat> buffersListChannelStats(uint32_t machineId) const;
+    BufferManager::ChannelStat             buffersGetChannelStat(uint32_t machineId, uint32_t channelId) const;
+
+    // ===== 批次管理(新增) =====
+    struct BatchRec {
+        Proto::BatchState state = Proto::BatchState::Idle; // Idle / Active
+        std::string       activeBatchId;                   // Idle 时为空
+        uint64_t          activeStartTs = 0;               // ms epoch
+        uint64_t          expectedEndTs = 0;               // 0: 未知
+    };
+
+    // 新:仅传“预计时长ms”(0=未知),开始时间内部取 nowMs()
+    void batchStart(uint32_t machineId,
+        const std::string& batchId,
+        uint64_t expectedDurationMs = 0);
+
+    // (可选兼容)旧签名保留,但标记为弃用:内部转调新签名
+    void batchStart(uint32_t machineId,
+        const std::string& batchId,
+        uint64_t startTs /*ignored*/,
+        uint64_t expectedEndTs /*treated as duration or absolute?*/);
+
+// 结束当前批次(保持数据,状态转 Idle)
+    void batchStop(uint32_t machineId,
+        uint64_t endTs = 0);                    // 备用
+
+// 查询该机当前批次(若不存在,返回 Idle 空记录)
+    BatchRec getBatch(uint32_t machineId) const;
+
+private:
+    struct ClientInfo {
+        SOCKET sock = INVALID_SOCKET;
+        std::string ip;
+        uint16_t port = 0;
+        std::chrono::steady_clock::time_point tsConnected{};
+        bool versionOk = false;
+        FrameAssembler fr; // 每个客户端一个帧组装器
+    };
+
+    SocketComm socketComm;
+    std::vector<ClientInfo> clients;
+
+    std::function<void(int, std::string)> cbStatus;
+    std::function<void(const std::vector<uint8_t>&)> cbRaw;
+    bool rawDumpEnabled = true;
+
+    std::function<void(const std::string&, uint16_t, bool)> cbClientEvent;
+
+    std::thread       worker;
+    std::atomic<bool> running{ false };
+    std::mutex        mClients;   // 保护 clients
+
+    BufferRegistry registry_;     // 一个 Collector 管所有机台
+
+    // 批次状态
+    std::unordered_map<uint32_t, BatchRec> batches_;
+    mutable std::mutex mBatches;
+
+    // 内部函数
+    void handleClientData(ClientInfo& c, const std::vector<uint8_t>& data);
+    bool isVersionRequest(const std::vector<uint8_t>& data) const;
+
+    // 统一错误回包(6 参数)
+    void sendErrorTo(ClientInfo& c,
+        uint32_t dataId,
+        uint16_t refCmd,
+        uint32_t machineId,
+        Proto::ErrCode code,
+        const std::string& message);
+
+    // 工具:当前 epoch 毫秒
+    static uint64_t nowMs();
+};
+
+#endif // COLLECTOR_H
diff --git a/SourceCode/Bond/DAQBridge/core/CommBase.h b/SourceCode/Bond/DAQBridge/core/CommBase.h
new file mode 100644
index 0000000..5795299
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/CommBase.h
@@ -0,0 +1,29 @@
+#pragma once
+#ifndef COMM_BASE_H
+#define COMM_BASE_H
+
+#include <string>
+#include <vector>
+#include <functional>
+
+class CommBase {
+public:
+    virtual void sendSampleData(double sample) = 0;
+    virtual void sendWindowData(const std::vector<std::string>& dataFields) = 0;
+    virtual void connectServer(const std::string& ip, uint16_t port) = 0;
+    virtual void createServer(uint16_t port) = 0;
+    virtual void disconnect() = 0;
+
+    // 连接状态回调
+    virtual void onConnectionEstablished() = 0;
+    virtual void onConnectionLost() = 0;
+    virtual void setConnectionStatusCallback(std::function<void(int, std::string)> callback) = 0;
+
+    // 新增:原始数据上抛(收到的“字节流”直接回调给应用层)
+    virtual void setRawDataCallback(std::function<void(const std::vector<uint8_t>&)> callback) = 0;
+
+    // 新增:开关(默认 true)
+    virtual void setRawDumpEnabled(bool enabled) = 0;
+};
+
+#endif // COMM_BASE_H
diff --git a/SourceCode/Bond/DAQBridge/core/ConnEvents.h b/SourceCode/Bond/DAQBridge/core/ConnEvents.h
new file mode 100644
index 0000000..2e29fda
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/ConnEvents.h
@@ -0,0 +1,40 @@
+#pragma once
+#include <string>
+
+namespace DAQEvt {
+
+    // 用枚举定义,外部回调仍以 int 传递
+    enum class ConnCode : int {
+        // Collector(服务端)
+        ServerListening = 100,  // createServer 成功
+        ServerStopped = 101,  // disconnect/close
+        ClientAccepted = 110,  // 新客户端接入
+        ClientDisconnected = 111,  // 客户端主动断开
+        VersionOk = 120,  // 客户端版本校验通过
+        VersionTimeoutKick = 121,  // 5 秒未握手被踢
+
+        // Display(客户端)
+        DisplayConnected = 200,  // connectServer 成功(已建连)
+        DisplayDisconnected = 201,  // disconnect/close
+
+        // 通用/错误
+        SocketError = 900
+    };
+
+    // 可选:把 code 转成默认字符串(备用)
+    inline const char* ToString(ConnCode c) {
+        switch (c) {
+        case ConnCode::ServerListening:     return "Server listening";
+        case ConnCode::ServerStopped:       return "Server stopped";
+        case ConnCode::ClientAccepted:      return "Client accepted";
+        case ConnCode::ClientDisconnected:  return "Client disconnected";
+        case ConnCode::VersionOk:           return "Version OK";
+        case ConnCode::VersionTimeoutKick:  return "Version timeout kick";
+        case ConnCode::DisplayConnected:    return "Display connected";
+        case ConnCode::DisplayDisconnected: return "Display disconnected";
+        case ConnCode::SocketError:         return "Socket error";
+        default:                            return "Unknown";
+        }
+    }
+
+} // namespace DAQEvt
diff --git a/SourceCode/Bond/DAQBridge/core/DataTypes.h b/SourceCode/Bond/DAQBridge/core/DataTypes.h
new file mode 100644
index 0000000..8943adc
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/DataTypes.h
@@ -0,0 +1,9 @@
+// DataTypes.h
+#pragma once
+#include <cstdint>
+
+struct Sample {
+    int64_t ts_ms = 0;  // 时间戳:毫秒
+    double  value = 0.0;
+};
+#pragma once
diff --git a/SourceCode/Bond/DAQBridge/core/Display.cpp b/SourceCode/Bond/DAQBridge/core/Display.cpp
new file mode 100644
index 0000000..9b3b71b
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/Display.cpp
@@ -0,0 +1,266 @@
+#include "Display.h"
+#include <iostream>
+#include "../DAQConfig.h"
+#include "../core/ConnEvents.h"
+using namespace DAQEvt;
+
+// 公共协议头
+#include "../proto/Protocol.h"
+#include "../proto/ProtocolCodec.h"
+#include "../net/FrameAssembler.h"
+
+using namespace Proto;
+
+Display::Display() {}
+Display::~Display() {
+    stopRecvLoop();
+    disconnect();
+}
+
+void Display::connectServer(const std::string& ip, uint16_t port) {
+    if (socketComm.createClientSocket(ip, port)) {
+        if (cbStatus) cbStatus(static_cast<int>(ConnCode::DisplayConnected), "Display connected to server");
+        onConnectionEstablished();
+
+        // 连接后立刻发送版本请求(0x0001),带 dataId 回显
+        ReqVersion vreq;
+        vreq.dataId = m_nextDataId++;
+        auto pkt = encodeRequestVersion(vreq);
+        socketComm.sendDataSingle(pkt);
+
+        startRecvLoop(DAQCfg::RecvLoopIntervalMs);
+    }
+}
+
+void Display::disconnect() {
+    stopRecvLoop();
+    socketComm.closeSocket();
+    if (cbStatus) cbStatus(static_cast<int>(ConnCode::DisplayDisconnected), "Display disconnected");
+    onConnectionLost();
+}
+
+void Display::startRecvLoop(uint32_t intervalMs) {
+    if (recvRunning.load()) return;
+    recvRunning.store(true);
+    recvThread = std::thread([this, intervalMs]() {
+        std::vector<uint8_t> chunk;
+        std::vector<uint8_t> frame;
+        FrameAssembler fr;
+
+        while (recvRunning.load()) {
+            chunk.clear();
+            if (socketComm.recvSingle(chunk) && !chunk.empty()) {
+                if (rawDumpEnabled && cbRaw) cbRaw(chunk);
+                fr.push(chunk);
+                while (fr.nextFrame(frame)) {
+                    if (rawDumpEnabled && cbRaw) cbRaw(frame);
+                    handleRawData(frame);
+                }
+            }
+            std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
+        }
+        });
+}
+
+void Display::stopRecvLoop() {
+    if (!recvRunning.load()) return;
+    recvRunning.store(false);
+    if (recvThread.joinable()) recvThread.join();
+}
+
+void Display::sendSampleData(double) { /* 客户端这边暂不发 */ }
+void Display::sendWindowData(const std::vector<std::string>&) { /* 同上 */ }
+
+void Display::onConnectionEstablished() { std::cout << "[Display] connected\n"; }
+void Display::onConnectionLost() { std::cout << "[Display] disconnected\n"; }
+
+// —— 低层接口:显式 dataId ——
+
+// 发送 0x0101(增量拉取)— 原版(不带 batchId)
+void Display::requestSince(uint32_t dataId, uint32_t machineId, uint32_t channelId,
+    uint64_t sinceTsExclusive, uint16_t maxCount) {
+    // 调到带 batchId 的重载,传空串 => 不附加 batchId
+    requestSince(dataId, machineId, channelId, sinceTsExclusive, std::string(), maxCount);
+}
+
+// 发送 0x0101(增量拉取)— ★ 新:带 batchId
+void Display::requestSince(uint32_t dataId, uint32_t machineId, uint32_t channelId,
+    uint64_t sinceTsExclusive, const std::string& batchId, uint16_t maxCount) {
+    ReqSince req;
+    req.dataId = dataId;
+    req.machineId = machineId;
+    req.channelId = channelId;
+    req.sinceTsExclusive = sinceTsExclusive;
+    req.maxCount = maxCount;
+    if (!batchId.empty()) {
+        req.flags = SINCE_FLAG_HAS_BATCH;
+        req.batchId = batchId;
+    }
+    else {
+        req.flags = 0;
+        req.batchId.clear();
+    }
+    auto pkt = encodeRequestSince(req);
+    socketComm.sendDataSingle(pkt);
+}
+
+void Display::requestMachines(uint32_t dataId) {
+    ReqMachines req; req.dataId = dataId;
+    auto pkt = encodeRequestMachines(req);
+    socketComm.sendDataSingle(pkt);
+}
+
+void Display::requestStats(uint32_t dataId, uint32_t machineId) {
+    ReqStats req; req.dataId = dataId; req.machineId = machineId; req.flags = 0;
+    auto pkt = encodeRequestStats(req);
+    socketComm.sendDataSingle(pkt);
+}
+
+// 整机多通道增量 — 原版(不带 batchId)
+void Display::requestSinceAll(uint32_t dataId, uint32_t machineId, uint64_t sinceTsExclusive,
+    uint16_t maxPerChannel) {
+    // 调到带 batchId 的重载,传空串
+    requestSinceAll(dataId, machineId, sinceTsExclusive, std::string(), maxPerChannel);
+}
+
+// 整机多通道增量 — ★ 新:带 batchId
+void Display::requestSinceAll(uint32_t dataId, uint32_t machineId, uint64_t sinceTsExclusive,
+    const std::string& batchId, uint16_t maxPerChannel) {
+    ReqSinceAll req;
+    req.dataId = dataId;
+    req.machineId = machineId;
+    req.sinceTsExclusive = sinceTsExclusive;
+    req.maxPerChannel = maxPerChannel;
+    if (!batchId.empty()) {
+        req.flags = SINCE_FLAG_HAS_BATCH;
+        req.batchId = batchId;
+    }
+    else {
+        req.flags = 0;
+        req.batchId.clear();
+    }
+    socketComm.sendDataSingle(encodeRequestSinceAll(req));
+}
+
+// —— 新增:批次信息拉取 ——
+// 显式 dataId
+void Display::requestBatchInfo(uint32_t dataId, uint32_t machineId) {
+    ReqBatchInfo req; req.dataId = dataId; req.machineId = machineId;
+    socketComm.sendDataSingle(encodeRequestBatchInfo(req));
+}
+// 便捷:自动 dataId
+void Display::requestBatchInfo(uint32_t machineId) {
+    requestBatchInfo(m_nextDataId++, machineId);
+}
+
+// —— 便捷接口:自动分配 dataId ——
+// 三组只是简单地在内部调用上述显式版
+
+void Display::requestMachines() {
+    requestMachines(m_nextDataId++);
+}
+
+void Display::requestStats(uint32_t machineId) {
+    requestStats(m_nextDataId++, machineId);
+}
+
+void Display::requestSince(uint32_t machineId, uint32_t channelId,
+    uint64_t sinceTsExclusive, uint16_t maxCount) {
+    requestSince(m_nextDataId++, machineId, channelId, sinceTsExclusive, maxCount);
+}
+
+void Display::requestSince(uint32_t machineId, uint32_t channelId,
+    uint64_t sinceTsExclusive, const std::string& batchId, uint16_t maxCount) {
+    requestSince(m_nextDataId++, machineId, channelId, sinceTsExclusive, batchId, maxCount);
+}
+
+void Display::requestSinceAll(uint32_t machineId, uint64_t sinceTsExclusive, uint16_t maxPerChannel) {
+    requestSinceAll(m_nextDataId++, machineId, sinceTsExclusive, maxPerChannel);
+}
+
+void Display::requestSinceAll(uint32_t machineId, uint64_t sinceTsExclusive,
+    const std::string& batchId, uint16_t maxPerChannel) {
+    requestSinceAll(m_nextDataId++, machineId, sinceTsExclusive, batchId, maxPerChannel);
+}
+
+// 收包分发(在接收线程里被调用)
+void Display::handleRawData(const std::vector<uint8_t>& rawData) {
+    // F001 版本响应
+    {
+        RspVersion vrsp;
+        if (decodeResponseVersion(rawData, vrsp)) {
+            if (cbStatus) cbStatus(static_cast<int>(ConnCode::VersionOk),
+                std::string("Server version: ") + vrsp.version);
+            // m_versionOk = true;
+            return;
+        }
+    }
+
+    // F101 —— since 拉取响应
+    {
+        RspSince rsp;
+        if (decodeResponseSince(rawData, rsp)) {
+            if (cbSamples) cbSamples(rsp.machineId, rsp.channelId, rsp.lastTsSent, rsp.more, rsp.samples);
+            return;
+        }
+    }
+
+    // F103 —— 统计/通道
+    {
+        RspStats st;
+        if (decodeResponseStats(rawData, st)) {
+            if (cbStats) cbStats(st.machineId, st.channels);
+            return;
+        }
+    }
+
+    // F104 —— 机台列表
+    {
+        RspMachines ms;
+        if (decodeResponseMachines(rawData, ms)) {
+            if (cbMachines) cbMachines(ms.machines);
+            return;
+        }
+    }
+
+    // F105 —— 多通道增量
+    {
+        RspSinceAll ra;
+        if (decodeResponseSinceAll(rawData, ra)) {
+            if (cbSamplesMulti) cbSamplesMulti(ra.machineId, ra.moreAny, ra.blocks);
+            return;
+        }
+    }
+
+    // ★ 新增:F120 —— 批次信息
+    {
+        RspBatchInfo bi;
+        if (decodeResponseBatchInfo(rawData, bi)) {
+            if (cbBatchInfo) cbBatchInfo(bi);
+            return;
+        }
+    }
+
+    // ★ 新增:E100 —— 统一错误(自愈)
+    {
+        RspError er;
+        if (decodeResponseError(rawData, er)) {
+            if (cbStatus) {
+                std::string s = "ERR ref=0x" + [](uint16_t x) {
+                    char buf[8]; std::snprintf(buf, sizeof(buf), "%04X", (unsigned)x); return std::string(buf);
+                }(er.refCmd) +
+                    " mid=" + std::to_string(er.machineId) +
+                    " code=" + std::to_string((unsigned)er.code) +
+                    " msg=" + er.message;
+                cbStatus(static_cast<int>(ConnCode::SocketError), s);
+            }
+            // 错误自愈:NoActive / Mismatch → 拉一次 BatchInfo
+            if (er.code == ErrCode::NoActiveBatch || er.code == ErrCode::BatchMismatch) {
+                requestBatchInfo(er.machineId);
+            }
+            return;
+        }
+    }
+
+    // 其它类型(将来扩展)……
+}
diff --git a/SourceCode/Bond/DAQBridge/core/Display.h b/SourceCode/Bond/DAQBridge/core/Display.h
new file mode 100644
index 0000000..79d789b
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/core/Display.h
@@ -0,0 +1,113 @@
+#pragma once
+#ifndef DISPLAY_H
+#define DISPLAY_H
+
+#include "CommBase.h"
+#include "../net/SocketComm.h"
+#include <functional>
+#include <thread>
+#include <atomic>
+#include "../proto/Protocol.h"
+#include "../proto/ProtocolCodec.h"
+
+// 回调:收到样本的上抛(UI 用它画图/存本地)
+using SamplesCallback = std::function<void(uint32_t machineId, uint32_t channelId,
+    uint64_t lastTsSent, uint8_t more,
+    const std::vector<Proto::SamplePair>& samples)>;
+using SamplesMultiCallback = std::function<void(
+    uint32_t machineId, uint8_t moreAny,
+    const std::vector<Proto::ChannelBlock>& blocks)>;
+using MachinesCallback = std::function<void(const std::vector<Proto::MachineInfo>& machines)>;
+using StatsCallback = std::function<void(uint32_t machineId, const std::vector<Proto::ChannelStatInfo>& channels)>;
+
+// ★ 新增:批次信息回调
+using BatchInfoCallback = std::function<void(const Proto::RspBatchInfo&)>;
+
+class Display : public CommBase {
+public:
+    Display();
+    ~Display();
+
+    void sendSampleData(double sample) override;
+    void sendWindowData(const std::vector<std::string>& dataFields) override;
+
+    void connectServer(const std::string& ip, uint16_t port) override;
+    void createServer(uint16_t /*port*/) override {}
+    void disconnect() override;
+
+    void onConnectionEstablished() override;
+    void onConnectionLost() override;
+
+    void handleRawData(const std::vector<uint8_t>& rawData);
+
+    void setConnectionStatusCallback(std::function<void(int, std::string)> cb) override { cbStatus = std::move(cb); }
+    void setRawDataCallback(std::function<void(const std::vector<uint8_t>&)> cb) override { cbRaw = std::move(cb); }
+    void setRawDumpEnabled(bool enabled) override { rawDumpEnabled = enabled; }
+
+    void startRecvLoop(uint32_t intervalMs = 10);
+    void stopRecvLoop();
+
+    void setSamplesCallback(SamplesCallback cb) { cbSamples = std::move(cb); }
+    void setSamplesMultiCallback(SamplesMultiCallback cb) { cbSamplesMulti = std::move(cb); }
+    void setMachinesCallback(MachinesCallback cb) { cbMachines = std::move(cb); }
+    void setStatsCallback(StatsCallback cb) { cbStats = std::move(cb); }
+    void setBatchInfoCallback(BatchInfoCallback cb) { cbBatchInfo = std::move(cb); } // ★ 新增
+
+    // —— 原有“需显式 dataId”的低层接口(保留以兼容)——
+    void requestMachines(uint32_t dataId);
+    void requestStats(uint32_t dataId, uint32_t machineId);
+    void requestSince(uint32_t dataId, uint32_t machineId, uint32_t channelId,
+        uint64_t sinceTsExclusive, uint16_t maxCount = 1024);
+    // ★ 新增:带 batchId 的显式 dataId 版
+    void requestSince(uint32_t dataId, uint32_t machineId, uint32_t channelId,
+        uint64_t sinceTsExclusive, const std::string& batchId, uint16_t maxCount = 1024);
+
+    // 便捷:整机多通道增量(显式 dataId)
+    void requestSinceAll(uint32_t dataId, uint32_t machineId, uint64_t sinceTsExclusive,
+        uint16_t maxPerChannel = 1024);
+    // ★ 新增:带 batchId 的显式 dataId 版
+    void requestSinceAll(uint32_t dataId, uint32_t machineId, uint64_t sinceTsExclusive,
+        const std::string& batchId, uint16_t maxPerChannel = 1024);
+
+    // —— 新增:批次信息拉取(显式/便捷)——
+    void requestBatchInfo(uint32_t dataId, uint32_t machineId); // 显式 dataId
+    void requestBatchInfo(uint32_t machineId);                  // 便捷:自动 m_nextDataId++
+
+    // —— 新增:便捷高层接口(自动分配 dataId)——
+    void requestMachines(); // 自动 m_nextDataId++
+    void requestStats(uint32_t machineId);
+    void requestSince(uint32_t machineId, uint32_t channelId,
+        uint64_t sinceTsExclusive, uint16_t maxCount = 1024);
+    // ★ 新增:便捷带 batchId
+    void requestSince(uint32_t machineId, uint32_t channelId,
+        uint64_t sinceTsExclusive, const std::string& batchId, uint16_t maxCount = 1024);
+
+    // 便捷:整机多通道增量
+    void requestSinceAll(uint32_t machineId, uint64_t sinceTsExclusive,
+        uint16_t maxPerChannel = 1024);
+    // ★ 新增:便捷带 batchId
+    void requestSinceAll(uint32_t machineId, uint64_t sinceTsExclusive,
+        const std::string& batchId, uint16_t maxPerChannel = 1024);
+
+private:
+    SocketComm socketComm;
+    std::function<void(int, std::string)> cbStatus;
+
+    // 原始数据回调 & 开关
+    std::function<void(const std::vector<uint8_t>&)> cbRaw;
+    bool rawDumpEnabled = true;
+
+    std::thread recvThread;
+    std::atomic<bool> recvRunning{ false };
+    SamplesCallback      cbSamples;
+    SamplesMultiCallback cbSamplesMulti;
+    MachinesCallback     cbMachines;
+    StatsCallback        cbStats;
+    BatchInfoCallback    cbBatchInfo; // ★ 新增
+
+    // dataId 递增,用于配对请求/响应
+    uint32_t m_nextDataId = 1;
+    bool m_versionOk = false;
+};
+
+#endif // DISPLAY_H
diff --git a/SourceCode/Bond/DAQBridge/net/FrameAssembler.h b/SourceCode/Bond/DAQBridge/net/FrameAssembler.h
new file mode 100644
index 0000000..dd68684
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/net/FrameAssembler.h
@@ -0,0 +1,58 @@
+#pragma once
+#include <vector>
+#include <cstdint>
+#include <algorithm>
+
+class FrameAssembler {
+public:
+    // 追加一块原始字节
+    void push(const std::vector<uint8_t>& chunk) {
+        buf_.insert(buf_.end(), chunk.begin(), chunk.end());
+    }
+    void push(const uint8_t* p, size_t n) {
+        buf_.insert(buf_.end(), p, p + n);
+    }
+
+    // 提取下一帧;返回 true 表示 out 拿到了一帧完整数据
+    bool nextFrame(std::vector<uint8_t>& out) {
+        const uint8_t HEAD[4] = { 0x11,0x88,0x11,0x88 };
+        const uint8_t TAIL = 0x88;
+        for (;;) {
+            // 需要至少 4B 头 + 4B dataId + 2B len + 1B 尾 = 11B
+            if (buf_.size() < 11) return false;
+
+            // 找头同步
+            size_t i = 0;
+            while (i + 4 <= buf_.size() && !std::equal(HEAD, HEAD + 4, buf_.begin() + i)) ++i;
+            if (i + 4 > buf_.size()) { // 没找到头,清空
+                buf_.clear();
+                return false;
+            }
+            if (i > 0) buf_.erase(buf_.begin(), buf_.begin() + i); // 丢弃头前噪声
+
+            if (buf_.size() < 11) return false; // 还不够最小帧
+
+            // 读取正文长度(大端)
+            uint16_t len = (uint16_t(buf_[8]) << 8) | buf_[9];
+            size_t total = 4 + 4 + 2 + size_t(len) + 1; // 整帧长度
+            if (buf_.size() < total) return false; // 半帧,等下次
+
+            // 校验尾
+            if (buf_[total - 1] != TAIL) {
+                // 尾不对:丢弃一个字节,重新找头(避免死锁)
+                buf_.erase(buf_.begin());
+                continue;
+            }
+
+            // 取出完整帧
+            out.assign(buf_.begin(), buf_.begin() + total);
+            buf_.erase(buf_.begin(), buf_.begin() + total);
+            return true;
+        }
+    }
+
+    void clear() { buf_.clear(); }
+
+private:
+    std::vector<uint8_t> buf_;
+};
diff --git a/SourceCode/Bond/DAQBridge/net/SocketComm.cpp b/SourceCode/Bond/DAQBridge/net/SocketComm.cpp
new file mode 100644
index 0000000..43eac34
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/net/SocketComm.cpp
@@ -0,0 +1,105 @@
+#include "SocketComm.h"
+
+SocketComm::SocketComm() {
+    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
+        std::cerr << "WSAStartup failed\n";
+    }
+}
+SocketComm::~SocketComm() {
+    closeSocket();
+    WSACleanup();
+}
+
+bool SocketComm::setNonBlocking(SOCKET s, bool nb) {
+    u_long mode = nb ? 1UL : 0UL;
+    return ioctlsocket(s, FIONBIO, &mode) == 0;
+}
+
+bool SocketComm::createClientSocket(const std::string& serverIP, uint16_t serverPort) {
+    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+    if (sock == INVALID_SOCKET) return false;
+
+    sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(serverPort);
+    if (InetPton(AF_INET, serverIP.c_str(), &addr.sin_addr) <= 0) return false;
+
+    if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
+        std::cerr << "connect failed: " << WSAGetLastError() << "\n";
+        closesocket(sock); sock = INVALID_SOCKET; return false;
+    }
+    return true;
+}
+
+bool SocketComm::createServerSocket(uint16_t port) {
+    listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+    if (listenSock == INVALID_SOCKET) return false;
+
+    // 复用地址
+    BOOL yes = 1; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
+
+    sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port);
+    if (bind(listenSock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) return false;
+    if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) return false;
+
+    // 非阻塞监听
+    setNonBlocking(listenSock, true);
+    std::cout << "Server listening on port " << port << "\n";
+    return true;
+}
+
+bool SocketComm::acceptOne(SOCKET& outClient, std::string& outIp, uint16_t& outPort) {
+    sockaddr_in caddr{}; int len = sizeof(caddr);
+    SOCKET cs = accept(listenSock, (sockaddr*)&caddr, &len);
+    if (cs == INVALID_SOCKET) {
+        int e = WSAGetLastError();
+        if (e == WSAEWOULDBLOCK) return false; // 当前没有新连接
+        std::cerr << "accept error: " << e << "\n"; return false;
+    }
+    setNonBlocking(cs, true);
+    char ipbuf[INET_ADDRSTRLEN]{};
+    InetNtop(AF_INET, &caddr.sin_addr, ipbuf, INET_ADDRSTRLEN);
+    outIp = ipbuf; outPort = ntohs(caddr.sin_port);
+    outClient = cs;
+    return true;
+}
+
+bool SocketComm::recvFrom(SOCKET s, std::vector<uint8_t>& buffer, bool& peerClosed) {
+    peerClosed = false;
+    char tmp[4096];
+    int r = recv(s, tmp, sizeof(tmp), 0);
+    if (r > 0) {
+        buffer.assign(tmp, tmp + r);
+        return true;
+    }
+    if (r == 0) { // 对端正常关闭
+        peerClosed = true;
+        return false;
+    }
+    int e = WSAGetLastError();
+    if (e == WSAEWOULDBLOCK) return false; // 暂无数据
+    // 其它错误也认为连接不可用了
+    peerClosed = true;
+    return false;
+}
+
+bool SocketComm::sendTo(SOCKET s, const std::vector<uint8_t>& data) {
+    int sent = send(s, reinterpret_cast<const char*>(data.data()), (int)data.size(), 0);
+    return sent == (int)data.size();
+}
+
+void SocketComm::closeClient(SOCKET s) {
+    if (s != INVALID_SOCKET) closesocket(s);
+}
+
+void SocketComm::closeSocket() {
+    if (listenSock != INVALID_SOCKET) { closesocket(listenSock); listenSock = INVALID_SOCKET; }
+    if (sock != INVALID_SOCKET) { closesocket(sock);       sock = INVALID_SOCKET; }
+}
+
+bool SocketComm::sendDataSingle(const std::vector<uint8_t>& data) {
+    if (sock == INVALID_SOCKET) return false;
+    return sendTo(sock, data);
+}
+bool SocketComm::recvSingle(std::vector<uint8_t>& buffer) {
+    if (sock == INVALID_SOCKET) return false;
+    return recvFrom(sock, buffer);
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/DAQBridge/net/SocketComm.h b/SourceCode/Bond/DAQBridge/net/SocketComm.h
new file mode 100644
index 0000000..7e6746e
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/net/SocketComm.h
@@ -0,0 +1,48 @@
+#pragma once
+#ifndef SOCKET_COMM_H
+#define SOCKET_COMM_H
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <string>
+#include <vector>
+#include <iostream>
+
+#pragma comment(lib, "ws2_32.lib")
+
+class SocketComm {
+public:
+    SocketComm();
+    ~SocketComm();
+
+    bool createClientSocket(const std::string& serverIP, uint16_t serverPort); // 客户端
+    bool createServerSocket(uint16_t port);                                     // 服务端(监听)
+
+    // 新增:非阻塞 accept/收发/关闭指定客户端
+    bool acceptOne(SOCKET& outClient, std::string& outIp, uint16_t& outPort);
+    bool recvFrom(SOCKET s, std::vector<uint8_t>& buffer, bool& peerClosed);
+
+    // 可选:为了兼容旧调用,保留一个内联包装(如果其它地方还用到了旧签名)
+    inline bool recvFrom(SOCKET s, std::vector<uint8_t>& buffer) {
+        bool closed = false;
+        return recvFrom(s, buffer, closed);
+    }
+    bool sendTo(SOCKET s, const std::vector<uint8_t>& data);
+    void closeClient(SOCKET s);
+
+    void closeSocket(); // 关闭监听或单连接
+
+    // 供上层判断本对象当前是否是“监听模式”
+    bool isListening() const { return listenSock != INVALID_SOCKET; }
+    bool sendDataSingle(const std::vector<uint8_t>& data); // 客户端单连接发送
+    bool recvSingle(std::vector<uint8_t>& buffer);         // 客户端单连接接收
+
+private:
+    SOCKET listenSock = INVALID_SOCKET; // 监听 socket(服务端)
+    SOCKET sock = INVALID_SOCKET; // 单连接模式(客户端时用)
+    WSADATA wsaData{};
+
+    bool setNonBlocking(SOCKET s, bool nb);
+};
+
+#endif // SOCKET_COMM_H
diff --git a/SourceCode/Bond/DAQBridge/proto/Protocol.h b/SourceCode/Bond/DAQBridge/proto/Protocol.h
new file mode 100644
index 0000000..ce98f98
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/proto/Protocol.h
@@ -0,0 +1,168 @@
+#pragma once
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace Proto {
+
+    // 固定帧
+    inline constexpr uint8_t kHead[4] = { 0x11, 0x88, 0x11, 0x88 };
+    inline constexpr uint8_t kTail = 0x88;
+
+    // 指令码
+    enum : uint16_t {
+        // 握手
+        CMD_REQ_VERSION = 0x0001,
+        CMD_RSP_VERSION = 0xF001,
+
+        // 单通道增量拉取
+        CMD_REQ_SINCE = 0x0101,
+        CMD_RSP_SINCE = 0xF101,
+
+        // 统计/通道列表
+        CMD_REQ_STATS = 0x0103,
+        CMD_RSP_STATS = 0xF103,
+
+        // 机台列表
+        CMD_REQ_MACHINES = 0x0104,
+        CMD_RSP_MACHINES = 0xF104,
+
+        // 整机多通道增量拉取
+        CMD_REQ_SINCE_ALL = 0x0105,
+        CMD_RSP_SINCE_ALL = 0xF105,
+
+        // 批次信息
+        CMD_REQ_BATCH_INFO = 0x0120,
+        CMD_RSP_BATCH_INFO = 0xF120,
+
+        // 错误
+        CMD_RSP_ERROR = 0xE100,
+    };
+
+    // === since* 请求里附加 batchId 的标志位 ===
+    inline constexpr uint16_t SINCE_FLAG_HAS_BATCH = 0x0001;
+
+    // ===== 数据结构(两端共用) =====
+
+    // 0x0001 / 0xF001
+    struct ReqVersion {
+        uint32_t dataId = 0;
+    };
+    struct RspVersion {
+        uint32_t   dataId = 0;
+        std::string version;   // UTF-8,例如 "1.0.1"
+    };
+
+    // 0x0104 / 0xF104
+    struct MachineInfo {
+        uint32_t    id = 0;
+        std::string name;
+    };
+    struct ReqMachines {
+        uint32_t dataId = 0;
+    };
+    struct RspMachines {
+        uint32_t                dataId = 0;
+        std::vector<MachineInfo> machines;
+    };
+
+    // 0x0103 / 0xF103
+    struct ReqStats {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        uint16_t flags = 0;
+    };
+    struct ChannelStatInfo {
+        uint32_t    channelId = 0;
+        uint64_t    earliestTs = 0;
+        uint64_t    latestTs = 0;
+        uint32_t    size = 0;
+        std::string name;      // UTF-8
+    };
+    struct RspStats {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        std::vector<ChannelStatInfo> channels;
+    };
+
+    // 0x0101 / 0xF101(单通道增量)
+    struct ReqSince {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        uint32_t channelId = 0;
+        uint64_t sinceTsExclusive = 0;     // ms
+        uint16_t maxCount = 1024;
+        uint16_t flags = 0;     // 按位,见 SINCE_FLAG_HAS_BATCH
+        std::string batchId;               // flags & SINCE_FLAG_HAS_BATCH 时有效
+    };
+    struct SamplePair {
+        uint64_t ts_ms = 0;
+        double   value = 0.0;
+    };
+    struct RspSince {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        uint32_t channelId = 0;
+        uint64_t lastTsSent = 0;
+        uint8_t  more = 0;           // 该通道是否还有未发
+        std::vector<SamplePair> samples;
+    };
+
+    // 0x0105 / 0xF105(整机多通道增量)
+    struct ChannelBlock {
+        uint32_t channelId = 0;
+        uint64_t lastTsSent = 0;          // 该通道本次最后样本时间戳
+        uint8_t  more = 0;          // 该通道是否还有未发
+        std::vector<SamplePair> samples;
+    };
+    struct ReqSinceAll {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        uint64_t sinceTsExclusive = 0;     // 对所有通道统一 since
+        uint16_t maxPerChannel = 1024;  // 每条曲线上限
+        uint16_t flags = 0;     // 按位,见 SINCE_FLAG_HAS_BATCH
+        std::string batchId;               // flags & SINCE_FLAG_HAS_BATCH 时有效
+    };
+    struct RspSinceAll {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+        uint8_t  moreAny = 0;            // 是否还有任意通道有剩余
+        std::vector<ChannelBlock> blocks;  // 多个通道的增量
+    };
+
+    // === 批次状态 ===
+    enum class BatchState : uint8_t {
+        Idle = 0,  // 无活动批次:activeBatchId="", activeStartTs=0, expectedEndTs=0
+        Active = 1,
+    };
+
+    // === 错误码 ===
+    enum class ErrCode : uint16_t {
+        NoActiveBatch = 0x0001, // 当前无活动批次
+        BatchMismatch = 0x0002, // 请求携带的 batchId 与当前活动批次不一致
+    };
+
+    // 0x0120 / 0xF120(批次信息)
+    struct ReqBatchInfo {
+        uint32_t dataId = 0;
+        uint32_t machineId = 0;
+    };
+    struct RspBatchInfo {
+        uint32_t   dataId = 0;
+        uint32_t   machineId = 0;
+        BatchState state = BatchState::Idle;
+        std::string activeBatchId;         // state=Idle 时应为空
+        uint64_t   activeStartTs = 0;
+        uint64_t   expectedEndTs = 0;      // =0 表示未知/Idle
+    };
+
+    // 0xE100 错误帧
+    struct RspError {
+        uint32_t   dataId = 0;
+        uint16_t   refCmd = 0;          // 出错请求的指令:如 0x0101/0x0105/0x0120...
+        uint32_t   machineId = 0;
+        ErrCode    code = ErrCode::NoActiveBatch;
+        std::string message;               // 简短文本
+    };
+
+} // namespace Proto
diff --git a/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.cpp b/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.cpp
new file mode 100644
index 0000000..6a1d4a3
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.cpp
@@ -0,0 +1,663 @@
+#include "ProtocolCodec.h"
+#include <cstring>   // std::memcpy
+#include <cstdio>    // std::snprintf (如需日志)
+#include <vector>
+
+namespace Proto {
+
+    // ---------------------------
+    // Big-endian 基础编解码工具
+    // ---------------------------
+    void put_u16(std::vector<uint8_t>& v, uint16_t x) {
+        v.push_back(static_cast<uint8_t>((x >> 8) & 0xFF));
+        v.push_back(static_cast<uint8_t>(x & 0xFF));
+    }
+    void put_u32(std::vector<uint8_t>& v, uint32_t x) {
+        v.push_back(static_cast<uint8_t>((x >> 24) & 0xFF));
+        v.push_back(static_cast<uint8_t>((x >> 16) & 0xFF));
+        v.push_back(static_cast<uint8_t>((x >> 8) & 0xFF));
+        v.push_back(static_cast<uint8_t>(x & 0xFF));
+    }
+    void put_u64(std::vector<uint8_t>& v, uint64_t x) {
+        for (int i = 7; i >= 0; --i) v.push_back(static_cast<uint8_t>((x >> (i * 8)) & 0xFF));
+    }
+    uint16_t get_u16(const uint8_t* p) {
+        return static_cast<uint16_t>((uint16_t(p[0]) << 8) | uint16_t(p[1]));
+    }
+    uint32_t get_u32(const uint8_t* p) {
+        return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) | uint32_t(p[3]);
+    }
+    uint64_t get_u64(const uint8_t* p) {
+        uint64_t x = 0;
+        for (int i = 0; i < 8; ++i) { x = (x << 8) | p[i]; }
+        return x;
+    }
+    void     put_f64_be(std::vector<uint8_t>& v, double d) {
+        static_assert(sizeof(double) == 8, "double must be 8 bytes");
+        uint64_t u;
+        std::memcpy(&u, &d, 8);
+        for (int i = 7; i >= 0; --i) v.push_back(static_cast<uint8_t>((u >> (i * 8)) & 0xFF));
+    }
+    double   get_f64_be(const uint8_t* p) {
+        uint64_t u = 0;
+        for (int i = 0; i < 8; ++i) { u = (u << 8) | p[i]; }
+        double d;
+        std::memcpy(&d, &u, 8);
+        return d;
+    }
+
+    // 快速窥探正文 cmd(不做完整长度核对,保留你原本风格)
+    uint16_t peek_cmd(const std::vector<uint8_t>& f) {
+        if (f.size() < 12) return 0;
+        if (!(f[0] == kHead[0] && f[1] == kHead[1] && f[2] == kHead[2] && f[3] == kHead[3])) return 0;
+        return (uint16_t(f[10]) << 8) | f[11];
+    }
+
+    // 包装帧:4B头 + 4B dataId + 2B bodyLen + body + 1B尾
+    static inline void frame_wrap(uint32_t dataId, const std::vector<uint8_t>& body, std::vector<uint8_t>& out) {
+        out.clear();
+        out.reserve(4 + 4 + 2 + body.size() + 1);
+        out.push_back(kHead[0]); out.push_back(kHead[1]); out.push_back(kHead[2]); out.push_back(kHead[3]);
+        put_u32(out, dataId);
+        put_u16(out, static_cast<uint16_t>(body.size()));
+        out.insert(out.end(), body.begin(), body.end());
+        out.push_back(kTail);
+    }
+
+    // ---------------------------
+    // 0x0104 / 0xF104 Machines
+    // ---------------------------
+    std::vector<uint8_t> encodeRequestMachines(const ReqMachines& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_MACHINES);
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    std::vector<uint8_t> encodeResponseMachines(const RspMachines& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_MACHINES);
+        put_u16(body, static_cast<uint16_t>(rsp.machines.size()));
+        for (auto& m : rsp.machines) {
+            put_u32(body, m.id);
+            const uint16_t n = static_cast<uint16_t>(m.name.size());
+            put_u16(body, n);
+            body.insert(body.end(), m.name.begin(), m.name.end());
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestMachines(const std::vector<uint8_t>& f, ReqMachines& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        return (get_u16(b) == CMD_REQ_MACHINES);
+    }
+
+    bool decodeResponseMachines(const std::vector<uint8_t>& f, RspMachines& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_MACHINES) return false;
+        b += 2;
+        const uint16_t cnt = get_u16(b); b += 2;
+
+        out.machines.clear();
+        out.machines.reserve(cnt);
+        for (uint16_t i = 0; i < cnt; ++i) {
+            if (b + 4 + 2 > e) return false;
+            const uint32_t id = get_u32(b); b += 4;
+            const uint16_t n = get_u16(b); b += 2;
+            if (b + n > e) return false;
+            out.machines.push_back({ id, std::string(reinterpret_cast<const char*>(b), n) });
+            b += n;
+        }
+        return (b == e);
+    }
+
+    // ---------------------------
+    // 0x0001 / 0xF001 Version
+    // ---------------------------
+    std::vector<uint8_t> encodeRequestVersion(const ReqVersion& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_VERSION);
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    std::vector<uint8_t> encodeResponseVersion(const RspVersion& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_VERSION);
+        const uint16_t n = static_cast<uint16_t>(rsp.version.size());
+        put_u16(body, n);
+        body.insert(body.end(), rsp.version.begin(), rsp.version.end()); // UTF-8
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestVersion(const std::vector<uint8_t>& f, ReqVersion& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+        const uint8_t* b = p + 10;
+        return (get_u16(b) == CMD_REQ_VERSION);
+    }
+
+    bool decodeResponseVersion(const std::vector<uint8_t>& f, RspVersion& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_VERSION) return false;
+        b += 2;
+
+        const uint16_t n = get_u16(b); b += 2;
+        if (b + n > e) return false;
+        out.version.assign(reinterpret_cast<const char*>(b), n);
+        b += n;
+
+        return (b == e);
+    }
+
+    // ---------------------------
+    // 0x0103 / 0xF103 Stats
+    // ---------------------------
+    std::vector<uint8_t> encodeRequestStats(const ReqStats& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_STATS);
+        put_u32(body, req.machineId);
+        put_u16(body, req.flags);
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    std::vector<uint8_t> encodeResponseStats(const RspStats& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_STATS);
+        put_u32(body, rsp.machineId);
+        put_u16(body, static_cast<uint16_t>(rsp.channels.size()));
+
+        for (auto& c : rsp.channels) {
+            put_u32(body, c.channelId);
+            put_u64(body, c.earliestTs);
+            put_u64(body, c.latestTs);
+            put_u32(body, c.size);
+            const uint16_t n = static_cast<uint16_t>(c.name.size());
+            put_u16(body, n);
+            body.insert(body.end(), c.name.begin(), c.name.end());
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestStats(const std::vector<uint8_t>& f, ReqStats& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+        if (b + 2 + 4 + 2 > e) return false;
+        if (get_u16(b) != CMD_REQ_STATS) return false;
+        b += 2;
+
+        out.machineId = get_u32(b); b += 4;
+        out.flags = get_u16(b); b += 2;
+
+        return (b == e);
+    }
+
+    bool decodeResponseStats(const std::vector<uint8_t>& f, RspStats& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 4 + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_STATS) return false;
+        b += 2;
+
+        out.machineId = get_u32(b); b += 4;
+        const uint16_t cnt = get_u16(b); b += 2;
+
+        out.channels.clear();
+        out.channels.reserve(cnt);
+        for (uint16_t i = 0; i < cnt; ++i) {
+            if (b + 4 + 8 + 8 + 4 + 2 > e) return false;
+            ChannelStatInfo ci{};
+            ci.channelId = get_u32(b); b += 4;
+            ci.earliestTs = get_u64(b); b += 8;
+            ci.latestTs = get_u64(b); b += 8;
+            ci.size = get_u32(b); b += 4;
+
+            const uint16_t n = get_u16(b); b += 2;
+            if (b + n > e) return false;
+            ci.name.assign(reinterpret_cast<const char*>(b), n);
+            b += n;
+
+            out.channels.push_back(std::move(ci));
+        }
+        return (b == e);
+    }
+
+    // ---------------------------------------
+    // 0x0101 / 0xF101 Since(支持可选 batchId)
+    // ---------------------------------------
+    std::vector<uint8_t> encodeRequestSince(const ReqSince& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_SINCE);
+        put_u32(body, req.machineId);
+        put_u32(body, req.channelId);
+        put_u64(body, req.sinceTsExclusive);
+        put_u16(body, req.maxCount);
+        put_u16(body, req.flags);
+        if (req.flags & SINCE_FLAG_HAS_BATCH) {
+            const uint16_t L = static_cast<uint16_t>(req.batchId.size());
+            put_u16(body, L);
+            body.insert(body.end(), req.batchId.begin(), req.batchId.end());
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    std::vector<uint8_t> encodeResponseSince(const RspSince& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_SINCE);
+        put_u32(body, rsp.machineId);
+        put_u32(body, rsp.channelId);
+        put_u64(body, rsp.lastTsSent);
+        body.push_back(rsp.more ? 1u : 0u);
+        put_u16(body, static_cast<uint16_t>(rsp.samples.size()));
+        for (auto& s : rsp.samples) {
+            put_u64(body, s.ts_ms);
+            put_f64_be(body, s.value);
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestSince(const std::vector<uint8_t>& f, ReqSince& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 > e) return false;
+        if (get_u16(b) != CMD_REQ_SINCE) return false;
+        b += 2;
+
+        if (b + 4 + 4 + 8 + 2 + 2 > e) return false;
+        out.machineId = get_u32(b); b += 4;
+        out.channelId = get_u32(b); b += 4;
+        out.sinceTsExclusive = get_u64(b); b += 8;
+        out.maxCount = get_u16(b); b += 2;
+        out.flags = get_u16(b); b += 2;
+
+        out.batchId.clear();
+        if (out.flags & SINCE_FLAG_HAS_BATCH) {
+            if (b + 2 > e) return false;
+            const uint16_t L = get_u16(b); b += 2;
+            if (b + L > e) return false;
+            out.batchId.assign(reinterpret_cast<const char*>(b), L);
+            b += L;
+        }
+
+        return (b == e);
+    }
+
+    bool decodeResponseSince(const std::vector<uint8_t>& f, RspSince& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_SINCE) return false;
+        b += 2;
+
+        if (b + 4 + 4 + 8 + 1 + 2 > e) return false;
+        out.machineId = get_u32(b); b += 4;
+        out.channelId = get_u32(b); b += 4;
+        out.lastTsSent = get_u64(b); b += 8;
+        out.more = *b++; // u8
+        const uint16_t cnt = get_u16(b); b += 2;
+
+        out.samples.clear();
+        out.samples.reserve(cnt);
+        for (uint16_t i = 0; i < cnt; ++i) {
+            if (b + 8 + 8 > e) return false;
+            const uint64_t ts = get_u64(b); b += 8;
+            const double   v = get_f64_be(b); b += 8;
+            out.samples.push_back({ ts, v });
+        }
+        return (b == e);
+    }
+
+    // ---------------------------------------
+    // 0x0105 / 0xF105 SinceAll(整机多通道增量)
+    // ---------------------------------------
+    std::vector<uint8_t> encodeRequestSinceAll(const ReqSinceAll& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_SINCE_ALL);
+        put_u32(body, req.machineId);
+        put_u64(body, req.sinceTsExclusive);
+        put_u16(body, req.maxPerChannel);
+        put_u16(body, req.flags);
+        if (req.flags & SINCE_FLAG_HAS_BATCH) {
+            const uint16_t L = static_cast<uint16_t>(req.batchId.size());
+            put_u16(body, L);
+            body.insert(body.end(), req.batchId.begin(), req.batchId.end());
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    std::vector<uint8_t> encodeResponseSinceAll(const RspSinceAll& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_SINCE_ALL);
+        put_u32(body, rsp.machineId);
+        body.push_back(rsp.moreAny ? 1u : 0u);
+        put_u16(body, static_cast<uint16_t>(rsp.blocks.size()));
+
+        for (const auto& b : rsp.blocks) {
+            put_u32(body, b.channelId);
+            put_u64(body, b.lastTsSent);
+            body.push_back(b.more ? 1u : 0u);
+            put_u16(body, static_cast<uint16_t>(b.samples.size()));
+            for (const auto& sp : b.samples) {
+                put_u64(body, sp.ts_ms);
+                put_f64_be(body, sp.value);
+            }
+        }
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestSinceAll(const std::vector<uint8_t>& f, ReqSinceAll& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 > e) return false;
+        if (get_u16(b) != CMD_REQ_SINCE_ALL) return false;
+        b += 2;
+
+        if (b + 4 + 8 + 2 + 2 > e) return false;
+        out.machineId = get_u32(b); b += 4;
+        out.sinceTsExclusive = get_u64(b); b += 8;
+        out.maxPerChannel = get_u16(b); b += 2;
+        out.flags = get_u16(b); b += 2;
+
+        out.batchId.clear();
+        if (out.flags & SINCE_FLAG_HAS_BATCH) {
+            if (b + 2 > e) return false;
+            const uint16_t L = get_u16(b); b += 2;
+            if (b + L > e) return false;
+            out.batchId.assign(reinterpret_cast<const char*>(b), L);
+            b += L;
+        }
+        return (b == e);
+    }
+
+    bool decodeResponseSinceAll(const std::vector<uint8_t>& f, RspSinceAll& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_SINCE_ALL) return false;
+        b += 2;
+
+        if (b + 4 + 1 + 2 > e) return false;
+        out.machineId = get_u32(b); b += 4;
+        out.moreAny = *b++; // u8
+        const uint16_t N = get_u16(b); b += 2;
+
+        out.blocks.clear();
+        out.blocks.reserve(N);
+        for (uint16_t i = 0; i < N; ++i) {
+            if (b + 4 + 8 + 1 + 2 > e) return false;
+            ChannelBlock blk;
+            blk.channelId = get_u32(b); b += 4;
+            blk.lastTsSent = get_u64(b); b += 8;
+            blk.more = *b++; // u8
+            const uint16_t M = get_u16(b); b += 2;
+
+            blk.samples.clear();
+            blk.samples.reserve(M);
+            for (uint16_t j = 0; j < M; ++j) {
+                if (b + 8 + 8 > e) return false;
+                SamplePair sp;
+                sp.ts_ms = get_u64(b); b += 8;
+                sp.value = get_f64_be(b); b += 8;
+                blk.samples.push_back(sp);
+            }
+            out.blocks.push_back(std::move(blk));
+        }
+        return (b == e);
+    }
+
+    // ---------------------------
+    // 0x0120 / 0xF120 BatchInfo
+    // ---------------------------
+    std::vector<uint8_t> encodeRequestBatchInfo(const ReqBatchInfo& req) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_REQ_BATCH_INFO);
+        put_u32(body, req.machineId);
+
+        std::vector<uint8_t> out;
+        frame_wrap(req.dataId, body, out);
+        return out;
+    }
+
+    bool decodeRequestBatchInfo(const std::vector<uint8_t>& f, ReqBatchInfo& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 4 > e) return false;
+        if (get_u16(b) != CMD_REQ_BATCH_INFO) return false;
+        b += 2;
+
+        out.machineId = get_u32(b); b += 4;
+        return (b == e);
+    }
+
+    std::vector<uint8_t> encodeResponseBatchInfo(const RspBatchInfo& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_BATCH_INFO);
+        put_u32(body, rsp.machineId);
+        body.push_back(static_cast<uint8_t>(rsp.state));
+
+        const uint16_t L = static_cast<uint16_t>(rsp.activeBatchId.size());
+        put_u16(body, L);
+        body.insert(body.end(), rsp.activeBatchId.begin(), rsp.activeBatchId.end());
+
+        put_u64(body, rsp.activeStartTs);
+        put_u64(body, rsp.expectedEndTs);
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeResponseBatchInfo(const std::vector<uint8_t>& f, RspBatchInfo& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 4 + 1 + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_BATCH_INFO) return false;
+        b += 2;
+
+        out.machineId = get_u32(b); b += 4;
+        out.state = static_cast<BatchState>(*b++);
+
+        const uint16_t L = get_u16(b); b += 2;
+        if (b + L > e) return false;
+        out.activeBatchId.assign(reinterpret_cast<const char*>(b), L); b += L;
+
+        if (b + 8 + 8 > e) return false;
+        out.activeStartTs = get_u64(b); b += 8;
+        out.expectedEndTs = get_u64(b); b += 8;
+
+        return (b == e);
+    }
+
+    // ---------------------------
+    // 0xE100 Error
+    // ---------------------------
+    std::vector<uint8_t> encodeResponseError(const RspError& rsp) {
+        std::vector<uint8_t> body;
+        put_u16(body, CMD_RSP_ERROR);
+        put_u16(body, rsp.refCmd);
+        put_u32(body, rsp.machineId);
+        put_u16(body, static_cast<uint16_t>(rsp.code));
+        const uint16_t L = static_cast<uint16_t>(rsp.message.size());
+        put_u16(body, L);
+        body.insert(body.end(), rsp.message.begin(), rsp.message.end());
+
+        std::vector<uint8_t> out;
+        frame_wrap(rsp.dataId, body, out);
+        return out;
+    }
+
+    bool decodeResponseError(const std::vector<uint8_t>& f, RspError& out) {
+        if (f.size() < 11) return false;
+        const uint8_t* p = f.data();
+        if (p[0] != kHead[0] || p[1] != kHead[1] || p[2] != kHead[2] || p[3] != kHead[3]) return false;
+        if (f.back() != kTail) return false;
+
+        out.dataId = get_u32(p + 4);
+        const uint16_t bodyLen = get_u16(p + 8);
+        if (10u + bodyLen + 1u != f.size()) return false;
+
+        const uint8_t* b = p + 10;
+        const uint8_t* e = b + bodyLen;
+
+        if (b + 2 + 2 + 4 + 2 + 2 > e) return false;
+        if (get_u16(b) != CMD_RSP_ERROR) return false;
+        b += 2;
+
+        out.refCmd = get_u16(b); b += 2;
+        out.machineId = get_u32(b); b += 4;
+        out.code = static_cast<ErrCode>(get_u16(b)); b += 2;
+
+        const uint16_t L = get_u16(b); b += 2;
+        if (b + L > e) return false;
+        out.message.assign(reinterpret_cast<const char*>(b), L); b += L;
+
+        return (b == e);
+    }
+
+} // namespace Proto
diff --git a/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.h b/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.h
new file mode 100644
index 0000000..308898f
--- /dev/null
+++ b/SourceCode/Bond/DAQBridge/proto/ProtocolCodec.h
@@ -0,0 +1,49 @@
+#pragma once
+#include "Protocol.h"
+
+namespace Proto {
+
+	// 大端工具
+	void put_u16(std::vector<uint8_t>& v, uint16_t x);
+	void put_u32(std::vector<uint8_t>& v, uint32_t x);
+	void put_u64(std::vector<uint8_t>& v, uint64_t x);
+	uint16_t get_u16(const uint8_t* p);
+	uint32_t get_u32(const uint8_t* p);
+	uint64_t get_u64(const uint8_t* p);
+	void     put_f64_be(std::vector<uint8_t>& v, double d);
+	double   get_f64_be(const uint8_t* p);
+
+	// 帮助:peek cmd
+	uint16_t peek_cmd(const std::vector<uint8_t>& frame);
+
+	// 编码(构帧:头 + dataId + len + body + 尾)
+	std::vector<uint8_t> encodeRequestVersion(const ReqVersion& req);
+	std::vector<uint8_t> encodeResponseVersion(const RspVersion& rsp);
+	std::vector<uint8_t> encodeRequestMachines(const ReqMachines& req);
+	std::vector<uint8_t> encodeResponseMachines(const RspMachines& rsp);
+	std::vector<uint8_t> encodeRequestStats(const ReqStats& req);
+	std::vector<uint8_t> encodeResponseStats(const RspStats& rsp);
+	std::vector<uint8_t> encodeRequestSince(const ReqSince& req);
+	std::vector<uint8_t> encodeResponseSince(const RspSince& rsp);
+	std::vector<uint8_t> encodeRequestSinceAll(const ReqSinceAll& req);
+	std::vector<uint8_t> encodeResponseSinceAll(const RspSinceAll& rsp);
+	std::vector<uint8_t> encodeRequestBatchInfo(const ReqBatchInfo& req);
+	std::vector<uint8_t> encodeResponseBatchInfo(const RspBatchInfo& rsp);
+
+	// 解码
+	bool decodeRequestVersion(const std::vector<uint8_t>& frame, ReqVersion& out);
+	bool decodeResponseVersion(const std::vector<uint8_t>& frame, RspVersion& out);
+	bool decodeRequestMachines(const std::vector<uint8_t>& frame, ReqMachines& out);
+	bool decodeResponseMachines(const std::vector<uint8_t>& frame, RspMachines& out);
+	bool decodeRequestStats(const std::vector<uint8_t>& frame, ReqStats& out);
+	bool decodeResponseStats(const std::vector<uint8_t>& frame, RspStats& out);
+	bool decodeRequestSince(const std::vector<uint8_t>& frame, ReqSince& out);
+	bool decodeResponseSince(const std::vector<uint8_t>& frame, RspSince& out);
+	bool decodeRequestSinceAll(const std::vector<uint8_t>& frame, ReqSinceAll& out);
+	bool decodeResponseSinceAll(const std::vector<uint8_t>& frame, RspSinceAll& out);
+	bool decodeRequestBatchInfo(const std::vector<uint8_t>& frame, ReqBatchInfo& out);
+	bool decodeResponseBatchInfo(const std::vector<uint8_t>& frame, RspBatchInfo& out);
+	std::vector<uint8_t> encodeResponseError(const RspError& rsp);
+	bool decodeResponseError(const std::vector<uint8_t>& frame, RspError& out);
+
+} // namespace Proto
diff --git a/SourceCode/Bond/Servo/CBakeCooling.cpp b/SourceCode/Bond/Servo/CBakeCooling.cpp
index 166dedf..bbacd24 100644
--- a/SourceCode/Bond/Servo/CBakeCooling.cpp
+++ b/SourceCode/Bond/Servo/CBakeCooling.cpp
@@ -487,7 +487,53 @@
 
 	int CBakeCooling::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
 	{
-		return parsingParams(pszData, size, params);
+		ASSERT(pszData);
+		if (size < 250) return 0;
+		int i = 0, v;
+
+
+		// 1.工艺参数序号
+		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+		params.push_back(CParam("工艺参数序号", "", this->getName().c_str(), v));
+		i += 2;
+
+		if (v == 1) {
+			// 2.A_腔烘烤时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("A_腔烘烤时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+
+			// 3.A_烘烤温度设定
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("A_烘烤温度设定", "", this->getName().c_str(), v * 0.1f));
+			i += 2;
+		}
+		else if (v == 2) {
+			// 2.A_腔冷却时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("A_腔冷却时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+		}
+		else if (v == 3) {
+			// 2.A_腔烘烤时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("B_腔烘烤时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+
+			// 3.A_烘烤温度设定
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("B_烘烤温度设定", "", this->getName().c_str(), v * 0.1f));
+			i += 2;
+		}
+		else if (v == 4) {
+			// 2.A_腔冷却时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("B_腔冷却时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+		}
+
+
+		return (int)params.size();
 	}
 
 	int CBakeCooling::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
diff --git a/SourceCode/Bond/Servo/CBonder.cpp b/SourceCode/Bond/Servo/CBonder.cpp
index 797dada..b9f8343 100644
--- a/SourceCode/Bond/Servo/CBonder.cpp
+++ b/SourceCode/Bond/Servo/CBonder.cpp
@@ -487,9 +487,9 @@
 		return 0;
 	}
 
-	int CBonder::onProcessStateChanged(PROCESS_STATE state)
+	int CBonder::onProcessStateChanged(int slotNo, PROCESS_STATE state)
 	{
-		CEquipment::onProcessStateChanged(state);
+		CEquipment::onProcessStateChanged(slotNo, state);
 
 		if (state == PROCESS_STATE::Complete) {
 			// 检查数据,当前两片玻璃,一片为G1, 一片为G2, 且pProcessData中的id能匹配G1或G2
diff --git a/SourceCode/Bond/Servo/CBonder.h b/SourceCode/Bond/Servo/CBonder.h
index b8a9f08..89631dc 100644
--- a/SourceCode/Bond/Servo/CBonder.h
+++ b/SourceCode/Bond/Servo/CBonder.h
@@ -22,7 +22,7 @@
         virtual void getAttributeVector(CAttributeVector& attrubutes);
         virtual int recvIntent(CPin* pPin, CIntent* pIntent);
         virtual int onProcessData(CProcessData* pProcessData);
-        virtual int onProcessStateChanged(PROCESS_STATE state);
+        virtual int onProcessStateChanged(int slotNo, PROCESS_STATE state);
         virtual int getIndexerOperationModeBaseValue();
         virtual int parsingParams(const char* pszData, size_t size, std::vector<CParam>& parsms);
         virtual int parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& parsms);
diff --git a/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp b/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp
new file mode 100644
index 0000000..c44a1b5
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotGrid.cpp
@@ -0,0 +1,1083 @@
+锘�#include "stdafx.h"
+#include "CCarrierSlotGrid.h"
+#include <gdiplus.h>
+#pragma comment(lib, "gdiplus.lib")
+
+using namespace Gdiplus;
+
+#define SAFE_PORT(p) ((p) >= 0 && (p) < (int)m_ports.size())
+#define SAFE_SLOT(s) ((s) >= 0 && (s) < m_nSlots)
+
+static bool       s_gdiplusInited = false;
+static ULONG_PTR  s_gdiplusToken = 0;
+static void EnsureGdiplus()
+{
+    if (!s_gdiplusInited)
+    {
+        GdiplusStartupInput in;
+        if (GdiplusStartup(&s_gdiplusToken, &in, nullptr) == Ok)
+            s_gdiplusInited = true;
+    }
+}
+
+BEGIN_MESSAGE_MAP(CCarrierSlotGrid, CWnd)
+    ON_WM_ERASEBKGND()
+    ON_WM_PAINT()
+    ON_WM_CREATE()
+    ON_WM_SIZE()
+    ON_WM_HSCROLL()
+    ON_WM_VSCROLL()          // 鈽� 鏂板
+    ON_WM_LBUTTONDOWN()
+    ON_WM_LBUTTONUP()
+    ON_WM_MOUSEWHEEL()
+    ON_WM_MOUSEMOVE()
+    ON_WM_SETCURSOR()
+    ON_WM_SHOWWINDOW()
+    ON_WM_WINDOWPOSCHANGED()
+END_MESSAGE_MAP()
+
+CCarrierSlotGrid::CCarrierSlotGrid() {}
+CCarrierSlotGrid::~CCarrierSlotGrid()
+{
+    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
+    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
+    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
+}
+
+void CCarrierSlotGrid::PreSubclassWindow()
+{
+    CWnd::PreSubclassWindow();
+    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
+    EnsureFonts();
+    EnsureGdiplus();
+
+    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+}
+
+int CCarrierSlotGrid::OnCreate(LPCREATESTRUCT cs)
+{
+    if (CWnd::OnCreate(cs) == -1) return -1;
+    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
+    EnsureFonts();
+    EnsureGdiplus();
+
+    return 0;
+}
+
+void CCarrierSlotGrid::EnsureFonts()
+{
+    if ((HFONT)m_fntText) return;
+
+    LOGFONT lf{}; bool ok = false;
+    if (GetParent() && GetParent()->GetFont()) { GetParent()->GetFont()->GetLogFont(&lf); ok = true; }
+    else if (GetFont()) { GetFont()->GetLogFont(&lf); ok = true; }
+    else {
+        NONCLIENTMETRICS ncm = { 0 }; ncm.cbSize = sizeof(NONCLIENTMETRICS);
+        if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0)) { lf = ncm.lfMessageFont; ok = true; }
+    }
+    if (!ok) { CFont* pDef = CFont::FromHandle((HFONT)GetStockObject(DEFAULT_GUI_FONT)); pDef->GetLogFont(&lf); }
+
+    m_fntText.CreateFontIndirect(&lf);
+    LOGFONT lfb = lf; lfb.lfWeight = FW_SEMIBOLD;  m_fntBold.CreateFontIndirect(&lfb);
+    LOGFONT lfs = lf; lfs.lfHeight = (LONG)(lf.lfHeight * 0.9); if (lfs.lfHeight == 0) lfs.lfHeight = lf.lfHeight - 1; m_fntSmall.CreateFontIndirect(&lfs);
+}
+
+void CCarrierSlotGrid::InitGrid(int nPorts, int nSlots)
+{
+    ASSERT(nPorts >= 1 && nSlots >= 1);
+    m_ports.assign(nPorts, PortColumn{});
+    m_portColCXs.assign(nPorts, 180);
+    m_nSlots = nSlots;
+    for (auto& pc : m_ports) pc.slots.assign(m_nSlots, SlotCell{});
+    UpdateScrollRange();
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetColumnWidths(int slotColWidth, int portColWidth)
+{
+    if (slotColWidth > 0) m_slotColCX = slotColWidth;
+    if (portColWidth > 0)
+        for (size_t i = 0; i < m_portColCXs.size(); ++i) m_portColCXs[i] = portColWidth;
+    UpdateScrollRange();
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetRowHeight(int cy) { m_rowHeight = max(18, min(cy, 64)); UpdateScrollRange(); Invalidate(FALSE); }
+void CCarrierSlotGrid::SetHeaderHeight(int cy) { m_headerCY = max(20, min(cy, 48)); UpdateScrollRange(); Invalidate(FALSE); }
+void CCarrierSlotGrid::SetShowMaterialToggle(BOOL bShow) { m_bShowMatToggle = bShow; Invalidate(FALSE); }
+
+void CCarrierSlotGrid::SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    if (portName)    m_ports[portIndex].portName = portName;
+    if (carrierName) m_ports[portIndex].carrierName = carrierName;
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    auto& pc = m_ports[portIndex];
+    pc.allocated = !!allocated;
+    pc.allocatedBy = byName ? byName : _T("");
+    if (pc.allocated)
+        for (auto& cell : pc.slots) cell.checked = false;
+    Invalidate(FALSE);
+}
+
+BOOL CCarrierSlotGrid::IsPortAllocated(int portIndex) const
+{
+    if (!SAFE_PORT(portIndex)) return FALSE; return m_ports[portIndex].allocated;
+}
+
+void CCarrierSlotGrid::SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    cell.hasGlass = !!hasGlass;
+    cell.coreId = coreId ? coreId : _T("");
+    cell.material = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (!cell.hasGlass) cell.checked = false;
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify/* = FALSE*/)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    auto& cell = pc.slots[slotIndex];
+    if (!cell.hasGlass) return;
+    cell.checked = !!checked;
+    if(bNotify) NotifySelectionChanged(portIndex, slotIndex, cell.checked);
+    Invalidate(FALSE);
+}
+
+BOOL CCarrierSlotGrid::GetSlotChecked(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return FALSE;
+    return m_ports[portIndex].slots[slotIndex].checked ? TRUE : FALSE;
+}
+
+int CCarrierSlotGrid::GetSlotMaterialType(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
+    return m_ports[portIndex].slots[slotIndex].material;
+}
+
+void CCarrierSlotGrid::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    auto& cell = pc.slots[slotIndex];
+    if (!cell.hasGlass) return;
+    int mt = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (cell.material != mt) {
+        cell.material = mt;
+        if (bNotify) NotifyMaterialChanged(portIndex, slotIndex, cell.material);
+        Invalidate(FALSE);
+    }
+}
+
+CString CCarrierSlotGrid::GetDisplayId(int portIndex, int slotIndex) const
+{
+    CString s(_T("鈥�"));
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return s;
+    const auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return s;
+    s.Format(_T("G%d-%s"), (cell.material == MAT_G2) ? 2 : 1, cell.coreId.GetString());
+    return s;
+}
+
+void CCarrierSlotGrid::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    auto& pc = m_ports[portIndex];
+    if (pc.allocated) return;
+    for (int r = 0; r < m_nSlots; ++r) {
+        auto& cell = pc.slots[r];
+        if (!cell.hasGlass) continue;
+        if (cell.checked != !!checked) {
+            cell.checked = !!checked;
+            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
+        }
+    }
+    Invalidate(FALSE);
+}
+
+void CCarrierSlotGrid::RebuildTexts() { Invalidate(FALSE); }
+
+CSize CCarrierSlotGrid::CalcBestClientSize(int nSlotsOverride) const
+{
+    const int slots = (nSlotsOverride > 0) ? nSlotsOverride : m_nSlots;
+    int w = m_slotColCX;
+    for (int cx : m_portColCXs) w += cx;
+    int h = m_headerCY + slots * m_rowHeight;
+    return CSize(w, h);
+}
+
+CSize CCarrierSlotGrid::CalcBestWindowSize(BOOL includeNonClient,
+    int nSlotsOverride,
+    int extraPadX,
+    int extraPadY) const
+{
+    // 1) 鍩虹瀹㈡埛鍖哄昂瀵革紙鍚垜浠湪瀹㈡埛鍖虹敾鐨� 1px 杈规锛氬乏鍙�+2/涓婁笅+2锛�
+    const CSize content = CalcBestClientSize(nSlotsOverride);
+
+    // 2) 鍙� DPI銆佹粴鍔ㄦ潯灏哄锛堝敖閲忕敤 ForDpi锛屽洖閫�鍒版櫘閫氾級
+    UINT dpi = 96;
+#if (_WIN32_WINNT >= 0x0603)
+    if (m_hWnd) {
+        HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll");
+        if (hUser32) {
+            typedef UINT(WINAPI* PFN_GETDPIFORWINDOW)(HWND);
+            auto pGetDpiForWindow = (PFN_GETDPIFORWINDOW)::GetProcAddress(hUser32, "GetDpiForWindow");
+            if (pGetDpiForWindow) dpi = pGetDpiForWindow(m_hWnd);
+        }
+    }
+#endif
+    int cxVScroll = ::GetSystemMetrics(SM_CXVSCROLL);
+    int cyHScroll = ::GetSystemMetrics(SM_CYHSCROLL);
+#if (_WIN32_WINNT >= 0x0A00) // Win10: 鍙敤 GetSystemMetricsForDpi
+    HMODULE hUser32_2 = ::GetModuleHandleW(L"user32.dll");
+    if (hUser32_2) {
+        typedef int (WINAPI* PFN_GSMFD)(int, UINT);
+        auto pGsmForDpi = (PFN_GSMFD)::GetProcAddress(hUser32_2, "GetSystemMetricsForDpi");
+        if (pGsmForDpi) {
+            cxVScroll = pGsmForDpi(SM_CXVSCROLL, dpi);
+            cyHScroll = pGsmForDpi(SM_CYHSCROLL, dpi);
+        }
+    }
+#endif
+
+    // 3) DPI 鑷�傚簲瀹夊叏浣欓噺锛堥伩鍏嶅彇鏁磋宸�/涓婚宸紓锛�
+    const int autoPad = max(1, MulDiv(2, (int)dpi, 96)); // 绾︾瓑浜� 2px@96DPI
+    const int padX = (extraPadX >= 0) ? extraPadX : autoPad;
+    const int padY = (extraPadY >= 0) ? extraPadY : autoPad;
+
+    // 4) 杩唬锛氳�冭檻婊氬姩鏉$浉浜掑奖鍝嶏紝鐩村埌绋冲畾涓嶉渶瑕佹粴鍔ㄦ潯
+    int needCx = content.cx + padX;
+    int needCy = content.cy + padY;
+
+    while (true) {
+        bool needV = (GetTotalContentWidth() > needCx);                      // 瀹戒笉澶熲啋浼氬嚭鐜版í鍚戞粴鍔ㄦ潯锛燂紙娉ㄦ剰锛氭í鏉″崰楂樺害锛�
+        bool needH = (m_headerCY + (nSlotsOverride > 0 ? nSlotsOverride : m_nSlots) * m_rowHeight + 2 /*瀹㈡埛鍖鸿竟妗�*/ > needCy); // 楂樹笉澶熲啋浼氬嚭鐜扮旱鏉★紵锛堢旱鏉″崰瀹藉害锛�
+
+        // 娉ㄦ剰锛氬嚭鐜扳�滅旱鍚戞潯鈥濅細鍑忓皯鍙敤瀹藉害锛涘嚭鐜扳�滄í鍚戞潯鈥濅細鍑忓皯鍙敤楂樺害
+        // 鎴戜滑鐩爣鏄鈥滃嵆浣挎墸鎺夎繖浜涘崰浣嶁�濆悗涔熶粛鐒� >= 鍐呭灏哄
+        int adjCx = content.cx + padX + (needH ? cxVScroll : 0);
+        int adjCy = content.cy + padY + (needV ? cyHScroll : 0);
+
+        if (adjCx <= needCx && adjCy <= needCy) break; // 绋冲畾锛氬綋鍓� needCx/needCy 瓒冲
+        needCx = max(needCx, adjCx);
+        needCy = max(needCy, adjCy);
+    }
+
+    if (!includeNonClient) return CSize(needCx, needCy);
+
+    // 5) 鎶娾�滅悊鎯冲鎴峰尯灏哄鈥濇崲绠楁垚绐楀彛澶栨灏哄锛堝幓鎺� WS_H/VSCROLL 鍋氭崲绠楋級
+    RECT rc = { 0, 0, needCx, needCy };
+    DWORD style = GetStyle();          // 鉁� 鐢ㄧ湡瀹炴牱寮忥紝璁╃郴缁熸妸婊氬姩鏉¢潪瀹㈡埛鍖轰竴骞剁畻杩涘幓
+    DWORD exStyle = GetExStyle();
+    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
+    return CSize(rc.right - rc.left, rc.bottom - rc.top);
+}
+
+void CCarrierSlotGrid::DisableSystemScrollbars()
+{
+    // 鍘绘帀鏍峰紡
+    ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
+    // 闅愯棌锛堝吋瀹规�э級
+    ShowScrollBar(SB_HORZ, FALSE);
+    ShowScrollBar(SB_VERT, FALSE);
+    // 娓呯┖婊氬姩淇℃伅
+    SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL; si.nMin = 0; si.nMax = 0; si.nPage = 0; si.nPos = 0;
+    SetScrollInfo(SB_HORZ, &si, TRUE);
+    SetScrollInfo(SB_VERT, &si, TRUE);
+    // 璁╅潪瀹㈡埛鍖洪噸绠�
+    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+    Invalidate(FALSE);
+}
+
+// 娉ㄦ剰锛氳繖閲岀敤鈥滄棤婊氬姩鏉℃牱寮忊�濇潵鎹㈢畻绐楀彛澶栨灏哄锛岀‘淇濆鎴峰尯=鍐呭灏哄 + 鎴戜滑瀹㈡埛鍖鸿竟妗�
+void CCarrierSlotGrid::ResizeWindowToFitAll(BOOL includeNonClient, int nSlotsOverride)
+{
+    // 璁$畻鍐呭鎵�闇�瀹㈡埛鍖猴紙CalcBestClientSize 鍐呭凡鍖呭惈鎴戜滑瀹㈡埛鍖�1px杈规鐨� +2锛�
+    CSize need = CalcBestClientSize(nSlotsOverride);
+
+    if (!includeNonClient) {
+        SetWindowPos(nullptr, 0, 0, need.cx, need.cy,
+            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+        return;
+    }
+
+    RECT rc = { 0, 0, need.cx, need.cy };
+    DWORD style = GetStyle() & ~(WS_HSCROLL | WS_VSCROLL); // 鈫� 鐢ㄢ�滄棤婊氬姩鏉♀�濈殑鏍峰紡鏉ユ崲绠�
+    DWORD exStyle = GetExStyle();
+    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
+
+    int w = rc.right - rc.left;
+    int h = rc.bottom - rc.top;
+
+    SetWindowPos(nullptr, 0, 0, w, h,
+        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+}
+
+void CCarrierSlotGrid::SetNoScrollbarsMode(BOOL enable)
+{
+    m_noScrollbars = !!enable;
+
+    if (m_noScrollbars) {
+        // 1) 鍋忕Щ娓呴浂
+        m_scrollX = 0;
+        m_scrollY = 0;
+
+        // 2) 鍘绘帀鏍峰紡骞堕殣钘忔潯
+        ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
+        ShowScrollBar(SB_BOTH, FALSE);
+
+        // 3) 娓呯┖婊氬姩淇℃伅锛堝嵆渚胯祫婧愰噷鍘熸湰甯︿簡鏍峰紡锛屼篃涓嶅啀褰卞搷锛�
+        SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL;
+        SetScrollInfo(SB_HORZ, &si, TRUE);
+        SetScrollInfo(SB_VERT, &si, TRUE);
+
+        // 4) 閫氱煡绯荤粺闈炲鎴峰尯鍒锋柊锛岀‘淇濇潯琚交搴曠Щ闄�
+        SetWindowPos(nullptr, 0, 0, 0, 0,
+            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+
+        Invalidate(FALSE);
+    }
+    else {
+        // 閫�鍑烘棤婊氬姩鏉℃ā寮忥細浠呮仮澶嶆牱寮忥紝瀹為檯鑼冨洿浼氬湪 UpdateScrollRange 涓噸鏂拌缃�
+        ModifyStyle(0, WS_HSCROLL | WS_VSCROLL, 0);
+        SetWindowPos(nullptr, 0, 0, 0, 0,
+            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+        UpdateScrollRange();
+        Invalidate(FALSE);
+    }
+}
+
+void CCarrierSlotGrid::FitWindowToContentNoScroll(BOOL includeNonClient, int nSlotsOverride)
+{
+    // 纭繚宸插浜庘�滄棤婊氬姩鏉℃ā寮忊�濓紝闃叉绯荤粺鍦� AdjustWindowRectEx 鏃堕鐣欐粴鍔ㄦ潯闈炲鎴峰尯
+    SetNoScrollbarsMode(TRUE);
+
+    // 浣犺嚜宸辩殑 CalcBestClientSize 宸插寘鍚鎴峰尯 1px 杈规(+2)鐨勪慨姝�
+    CSize needCli = CalcBestClientSize(nSlotsOverride);
+
+    if (!includeNonClient) {
+        SetWindowPos(nullptr, 0, 0, needCli.cx, needCli.cy,
+            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+        return;
+    }
+
+    RECT rc{ 0, 0, needCli.cx, needCli.cy };
+    // 娉ㄦ剰锛氭鏃剁獥鍙f牱寮忓凡缁忔病鏈� WS_H/VSCROLL 浜嗏�斺�旂敤鐪熷疄鏍峰紡鎹㈢畻鍗冲彲
+    ::AdjustWindowRectEx(&rc, GetStyle(), FALSE, GetExStyle());
+    const int w = rc.right - rc.left;
+    const int h = rc.bottom - rc.top;
+
+    SetWindowPos(nullptr, 0, 0, w, h,
+        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
+}
+
+// ---------- 鍑犱綍 ----------
+CRect CCarrierSlotGrid::GetClientRectNoSB() const
+{
+    CRect rc; GetClientRect(&rc); return rc;
+}
+CRect CCarrierSlotGrid::GetHeaderRect() const
+{
+    CRect rc = GetClientRectNoSB(); rc.bottom = rc.top + m_headerCY; return rc;
+}
+int CCarrierSlotGrid::GetTotalContentWidth() const
+{
+    int w = m_slotColCX;
+    for (int cx : m_portColCXs) w += cx;
+    return w;
+}
+CRect CCarrierSlotGrid::GetHeaderItemRect(int iItem) const
+{
+    CRect rcHeader = GetHeaderRect();
+    int x = rcHeader.left - m_scrollX;
+    if (iItem == 0)
+        return CRect(x, rcHeader.top, x + m_slotColCX, rcHeader.bottom);
+
+    x += m_slotColCX;
+    for (int c = 1; c < iItem; ++c) x += m_portColCXs[c - 1];
+    int w = m_portColCXs[iItem - 1];
+    return CRect(x, rcHeader.top, x + w, rcHeader.bottom);
+}
+BOOL CCarrierSlotGrid::GetCellRect(int row, int sub, CRect& rc) const
+{
+    CRect cli = GetClientRectNoSB();
+    int y0 = cli.top + m_headerCY - m_scrollY;
+    int top = y0 + row * m_rowHeight;
+    int bottom = top + m_rowHeight;
+    if (bottom <= cli.top + m_headerCY || top >= cli.bottom) return FALSE;
+
+    int x = cli.left - m_scrollX;
+    if (sub == 0) { rc = CRect(x, top, x + m_slotColCX, bottom); return TRUE; }
+    x += m_slotColCX;
+    for (int c = 1; c < sub; ++c) x += m_portColCXs[c - 1];
+    int w = m_portColCXs[sub - 1];
+    rc = CRect(x, top, x + w, bottom);
+    return TRUE;
+}
+CRect CCarrierSlotGrid::GetHeaderCheckboxRect(int iItem) const
+{
+    CRect rItem = GetHeaderItemRect(iItem);
+    const int box = 16;
+    const int padR = 6;
+    int vpad = max(0, (rItem.Height() - box) / 2);
+    return CRect(rItem.right - padR - box, rItem.top + vpad, rItem.right - padR, rItem.bottom - vpad);
+}
+CRect CCarrierSlotGrid::GetCheckboxRect(const CRect& cell) const
+{
+    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
+    int leftPad = 8;
+    int top = cell.top + (cell.Height() - sz) / 2;
+    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
+}
+CRect CCarrierSlotGrid::GetMaterialTagRect(const CRect& cell) const
+{
+    int h = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
+    int w = 32;
+    int gap = 6;
+    int rightPadForDot = 16;
+    int top = cell.top + (cell.Height() - h) / 2;
+    int right = cell.right - rightPadForDot - gap;
+    return CRect(right - w, top, right, top + h);
+}
+CRect CCarrierSlotGrid::GetStatusDotRect(const CRect& cell) const
+{
+    int d = max(8, min(int(m_rowHeight * 0.42), 12));
+    int rightPad = 6;
+    int top = cell.top + (cell.Height() - d) / 2;
+    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
+}
+
+void CCarrierSlotGrid::UpdateScrollRange()
+{
+    if (m_noScrollbars) {
+        // 纭繚鍋忕Щ涓�鐩翠负 0锛屼笉璁句换浣曟粴鍔ㄤ俊鎭�
+        m_scrollX = 0;
+        m_scrollY = 0;
+        return;
+    }
+
+    CRect rc; GetClientRect(&rc);
+
+    // 鍨傜洿
+    const int contentH = m_headerCY + m_nSlots * m_rowHeight;
+    const int pageY = max(1, rc.Height());
+    const int maxPosY = max(0, contentH - pageY);
+    m_scrollY = max(0, min(m_scrollY, maxPosY));
+    SCROLLINFO siY = { sizeof(SCROLLINFO) };
+    siY.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
+    siY.nMin = 0; siY.nMax = contentH - 1; siY.nPage = pageY; siY.nPos = m_scrollY;
+    SetScrollInfo(SB_VERT, &siY, TRUE);
+
+    // 姘村钩
+    const int contentW = GetTotalContentWidth();
+    const int pageX = max(1, rc.Width());
+    const int maxPosX = max(0, contentW - pageX);
+    m_scrollX = max(0, min(m_scrollX, maxPosX));
+    SCROLLINFO siX = { sizeof(SCROLLINFO) };
+    siX.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
+    siX.nMin = 0; siX.nMax = contentW - 1; siX.nPage = pageX; siX.nPos = m_scrollX;
+    SetScrollInfo(SB_HORZ, &siX, TRUE);
+}
+
+// ---------- 琛ㄥご鍒嗛殧绾垮懡涓� ----------
+int CCarrierSlotGrid::HitHeaderEdge(CPoint pt) const
+{
+    if (!m_bAllowResize) return -1;  // 鈫� 鏂板
+    if (!GetHeaderRect().PtInRect(pt)) return -1;
+    const int tol = 4;
+    int x = GetHeaderRect().left - m_scrollX + m_slotColCX;
+    if (abs(pt.x - x) <= tol) return 0;
+
+    int cum = GetHeaderRect().left - m_scrollX + m_slotColCX;
+    for (int i = 0; i <= GetPortCount() - 2; ++i) {
+        cum += m_portColCXs[i];
+        if (abs(pt.x - cum) <= tol) return i + 1;
+    }
+    return -1;
+}
+
+// ---------- 缁樺埗 ----------
+BOOL CCarrierSlotGrid::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; }
+
+void CCarrierSlotGrid::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
+{
+    CBrush br(disabled ? RGB(245, 245, 245) : RGB(255, 255, 255));
+    CPen   pen(PS_SOLID, 1, disabled ? RGB(200, 200, 200) : RGB(130, 130, 135));
+    CBrush* pOldB = pDC->SelectObject(&br);
+    CPen* pOldP = pDC->SelectObject(&pen);
+    pDC->RoundRect(r, CPoint(3, 3));
+    pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
+
+    if (!checked) return;
+    COLORREF c = disabled ? RGB(160, 160, 160) : RGB(40, 150, 90);
+    CPen penTick(PS_SOLID, max(2, r.Height() / 8), c);
+    CPen* pOld = pDC->SelectObject(&penTick);
+    POINT p1 = { r.left + r.Width() * 2 / 9, r.top + r.Height() * 5 / 9 };
+    POINT p2 = { r.left + r.Width() * 4 / 9, r.top + r.Height() * 7 / 9 };
+    POINT p3 = { r.left + r.Width() * 7 / 9, r.top + r.Height() * 3 / 9 };
+    pDC->MoveTo(p1); pDC->LineTo(p2); pDC->LineTo(p3);
+    pDC->SelectObject(pOld);
+}
+
+void CCarrierSlotGrid::PaintTo(CDC* pDC)
+{
+    EnsureGdiplus();
+
+    CRect cli = GetClientRectNoSB();
+    pDC->FillSolidRect(cli, m_colBg);
+
+    // Header
+    CRect rh = GetHeaderRect();
+    pDC->FillSolidRect(rh, ::GetSysColor(COLOR_BTNFACE));
+    CPen penSep(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
+    CPen* pOldPen = pDC->SelectObject(&penSep);
+    pDC->MoveTo(rh.left, rh.bottom - 1); pDC->LineTo(rh.right, rh.bottom - 1);
+    pDC->SelectObject(pOldPen);
+
+    for (int i = 0; i <= GetPortCount(); ++i)
+    {
+        CRect rItem = GetHeaderItemRect(i);
+
+        // 淇敼涓猴細
+        if (i < GetPortCount()) { // 鈽� 鏈�鍚庝竴鍒椾笉鐢诲垎闅旂嚎
+            CPen pen(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
+            pOldPen = pDC->SelectObject(&pen);
+            pDC->MoveTo(rItem.right - 1, rItem.top);
+            pDC->LineTo(rItem.right - 1, rItem.bottom);
+            pDC->SelectObject(pOldPen);
+        }
+
+        CString text;
+        if (i == 0) {
+            text = _T("Slot");
+            CRect rt = rItem; rt.DeflateRect(6, 0, 6, 0);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(text, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+        }
+        else {
+            const auto& pc = m_ports[i - 1];
+            int selected = 0; bool any = false, all = true;
+            for (const auto& cell : pc.slots) {
+                if (cell.hasGlass) { any = true; if (cell.checked) ++selected; else all = false; }
+            }
+            if (!any) all = false;
+
+            CString leftTitle = pc.carrierName.IsEmpty()
+                ? pc.portName
+                : (pc.portName + _T(" (") + pc.carrierName + _T(")"));
+
+            // 鍕鹃�夋闈犲彸
+            CRect rcCb = GetHeaderCheckboxRect(i);
+            DrawFlatCheckbox(pDC, rcCb, all, pc.allocated);
+
+            // 璁℃暟璐磋繎鍕鹃�夋宸︿晶
+            CString cnt; cnt.Format(_T("%d/%d"), selected, m_nSlots);
+            SIZE szCnt{ 0,0 };
+            { CFont* o = pDC->SelectObject(&m_fntBold);
+            GetTextExtentPoint32(pDC->GetSafeHdc(), cnt, cnt.GetLength(), &szCnt);
+            pDC->SelectObject(o); }
+            const int gap = 6;
+            CRect rcCnt(rcCb.left - gap - szCnt.cx, rItem.top, rcCb.left - gap, rItem.bottom);
+
+            // 宸︿晶鏍囬
+            CRect rt = rItem; rt.DeflateRect(6, 0, (rItem.right - rcCnt.left) + 6, 0);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(leftTitle, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+
+            // 璁℃暟
+            pDC->SelectObject(&m_fntBold);
+            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
+            pDC->DrawText(cnt, rcCnt, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
+        }
+    }
+
+    // Cells
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        for (int s = 0; s <= GetPortCount(); ++s)
+        {
+            CRect rc;
+            if (!GetCellRect(r, s, rc)) continue;
+
+            COLORREF bk = m_colBg;
+            if (s >= 1) {
+                int port = s - 1;
+                if (port % 2 == 0) bk = m_colAlt;
+                if (SAFE_PORT(port) && m_ports[port].allocated) bk = m_colLock;
+            }
+            pDC->FillSolidRect(rc, bk);
+
+            CPen penMajor(PS_SOLID, (s >= 1 && ((s - 1) % 2 == 0)) ? 2 : 1, m_gridMajor);
+            CPen* pOld = pDC->SelectObject(&penMajor);
+            pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.right, rc.top);
+            pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.left, rc.bottom);
+            pDC->SelectObject(pOld);
+
+            CPen penMinor(PS_SOLID, 1, m_gridMinor);
+            pOld = pDC->SelectObject(&penMinor);
+            if (s == GetPortCount()) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
+            if (r == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
+            pDC->SelectObject(pOld);
+
+            if (s == 0) {
+                CString sl; sl.Format(_T("Slot %d"), r + 1);
+                CRect rt = rc; rt.DeflateRect(8, 0, 8, 0);
+                pDC->SelectObject(&m_fntBold);
+                pDC->SetBkMode(TRANSPARENT);
+                pDC->SetTextColor(RGB(60, 60, 64));
+                pDC->DrawText(sl, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
+            }
+            else {
+                int port = s - 1;
+                const auto& pc = m_ports[port];
+                const auto& cell = pc.slots[r];
+
+                CRect rChk = GetCheckboxRect(rc);
+                DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
+
+                CString t = GetDisplayId(port, r);
+                CRect rText = rc;
+                int leftPad = rChk.right + 6;
+                CRect rDot = GetStatusDotRect(rc);
+                CRect rTag = GetMaterialTagRect(rc);
+                int rightPad = rc.right - min(rTag.left - 6, rDot.left - 6);
+                rText.DeflateRect(leftPad - rc.left, 0, rightPad, 0);
+
+                pDC->SelectObject(&m_fntText);
+                pDC->SetBkMode(TRANSPARENT);
+                pDC->SetTextColor(cell.hasGlass ? m_text : m_textDim);
+                pDC->DrawText(t, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+
+                if (m_bShowMatToggle)
+                {
+                    CRect rT = GetMaterialTagRect(rc);
+                    COLORREF crBorder = (cell.material == MAT_G2) ? RGB(180, 150, 220) : RGB(120, 160, 220);
+                    COLORREF crFill = (cell.material == MAT_G2) ? RGB(243, 235, 250) : RGB(233, 240, 252);
+                    COLORREF crText = (cell.material == MAT_G2) ? RGB(90, 60, 150) : RGB(50, 90, 160);
+                    if (pc.allocated || !cell.hasGlass) { crBorder = RGB(210, 210, 210); crFill = RGB(245, 245, 245); crText = RGB(160, 160, 160); }
+                    CBrush br(crFill); CPen tagPen(PS_SOLID, 1, crBorder);
+                    CPen* pOldP = pDC->SelectObject(&tagPen);
+                    CBrush* pOldB = pDC->SelectObject(&br);
+                    pDC->RoundRect(rT, CPoint(6, 6));
+                    pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
+                    CString tx; tx.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
+                    pDC->SelectObject(&m_fntSmall);
+                    pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
+                    pDC->DrawText(tx, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
+                }
+
+                // 鐘舵�佺偣锛圙DI+ 鎶楅敮榻匡級
+                {
+                    Graphics g(pDC->GetSafeHdc());
+                    g.SetSmoothingMode(SmoothingModeAntiAlias);
+                    COLORREF c = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
+                    SolidBrush brush(Color(255, GetRValue(c), GetGValue(c), GetBValue(c)));
+                    Pen outline(Color(255, 120, 120, 120), 1.f);
+                    g.FillEllipse(&brush, rDot.left, rDot.top, rDot.Width(), rDot.Height());
+                    g.DrawEllipse(&outline, (REAL)rDot.left, (REAL)rDot.top, (REAL)rDot.Width(), (REAL)rDot.Height());
+                }
+            }
+        }
+    }
+
+
+
+// ===== 鍦ㄦ瘡涓凡鍒嗛厤(allocated)鐨勫垪涓ぎ缁樺埗鍗婇�忔槑 LOCK 姘村嵃锛堢敤 HDC+LOGFONT 鏋勯�犲瓧浣擄級=====
+    {
+        Gdiplus::Graphics g(pDC->GetSafeHdc());
+        g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
+
+        CRect cli = GetClientRectNoSB();
+        CRect rh = GetHeaderRect();
+
+        // 鍙栧綋鍓� UI 瀛椾綋锛堜紭鍏堢矖浣擄級
+        LOGFONT lf{};
+        if ((HFONT)m_fntBold)      m_fntBold.GetLogFont(&lf);
+        else if ((HFONT)m_fntText) m_fntText.GetLogFont(&lf);
+
+        for (int i = 0; i < GetPortCount(); ++i)
+        {
+            if (!m_ports[i].allocated) continue;
+
+            // 鍒楃煩褰紙闄ゅ幓琛ㄥご锛岃�冭檻姘村钩婊氬姩锛�
+            CRect rCol = GetHeaderItemRect(i + 1);
+            rCol.top = rh.bottom;
+            rCol.bottom = cli.bottom;
+            if (rCol.right <= cli.left || rCol.left >= cli.right || rCol.Height() <= 0) continue;
+
+            // 鑷�傚簲涓�涓悎閫傜殑鍍忕礌楂樺害
+            int availW = rCol.Width() - 12;
+            int availH = rCol.Height() - 12;
+            int emPx = max(16, min(min(availW / 3, availH / 5), 72));
+            if (emPx < 16) emPx = 16;
+            // 瀛楀彿鍑忓崐锛堝苟缁欎釜鏇翠綆鐨勫厹搴曪紝閬垮厤澶皬锛�
+            emPx = max(12, emPx / 2);
+
+            // 鐢� LOGFONTW + HDC 鏋勯�� GDI+ 瀛椾綋
+            LOGFONTW lfw{};
+#ifdef UNICODE
+            lfw = *reinterpret_cast<LOGFONTW*>(&lf);
+#else
+            lfw.lfHeight = lf.lfHeight;
+            lfw.lfWidth = lf.lfWidth;
+            lfw.lfEscapement = lf.lfEscapement;
+            lfw.lfOrientation = lf.lfOrientation;
+            lfw.lfWeight = lf.lfWeight;
+            lfw.lfItalic = lf.lfItalic;
+            lfw.lfUnderline = lf.lfUnderline;
+            lfw.lfStrikeOut = lf.lfStrikeOut;
+            lfw.lfCharSet = lf.lfCharSet;
+            lfw.lfOutPrecision = lf.lfOutPrecision;
+            lfw.lfClipPrecision = lf.lfClipPrecision;
+            lfw.lfQuality = lf.lfQuality;
+            lfw.lfPitchAndFamily = lf.lfPitchAndFamily;
+            MultiByteToWideChar(CP_ACP, 0, lf.lfFaceName, -1, lfw.lfFaceName, LF_FACESIZE);
+#endif
+            lfw.lfHeight = -emPx;           // 璐熷��=鎸夊儚绱犻珮搴�
+            lfw.lfWeight = FW_BOLD;
+
+            Gdiplus::Font gdifont(pDC->GetSafeHdc(), &lfw);    // 鈽� 鍔犱笂 Gdiplus::
+            Gdiplus::StringFormat fmt;
+            fmt.SetAlignment(Gdiplus::StringAlignmentCenter);
+            fmt.SetLineAlignment(Gdiplus::StringAlignmentCenter);
+            Gdiplus::Color col(140, 120, 100, 60);             // 鍗婇�忔槑
+            Gdiplus::SolidBrush brush(col);
+            Gdiplus::RectF box((Gdiplus::REAL)rCol.left, (Gdiplus::REAL)rCol.top,
+                (Gdiplus::REAL)rCol.Width(), (Gdiplus::REAL)rCol.Height());
+
+            if (gdifont.GetLastStatus() == Gdiplus::Ok) {
+                g.DrawString(L"LOCK", -1, &gdifont, box, &fmt, &brush);
+            }
+            else {
+                Gdiplus::Font fallback(L"Arial", (Gdiplus::REAL)emPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
+                g.DrawString(L"LOCK", -1, &fallback, box, &fmt, &brush);
+            }
+        }
+    }
+
+    // === 瀹㈡埛鍖哄唴 1px 鐏拌壊杈规锛堜笉鍖呮粴鍔ㄦ潯锛屼絾涓嶄細缂鸿/鎶㈢粯鍒讹級===
+    {
+        CRect cli; GetClientRect(&cli);
+
+        // 鐢� FrameRect 鏇寸ǔ锛堥伩鍏嶅彸涓嬭涓㈢嚎锛�
+        CBrush br; br.CreateSolidBrush(::GetSysColor(COLOR_3DSHADOW));
+        CRect r = cli;
+        // 娉ㄦ剰锛氬鎴峰尯鍧愭爣鏄� [0..width, 0..height]锛汧rameRect 浼氬湪鍐呬晶鐢� 1px
+        pDC->FrameRect(&r, &br);
+        br.DeleteObject();
+    }
+}
+
+void CCarrierSlotGrid::OnPaint()
+{
+    CPaintDC dc(this);
+    CRect rc; GetClientRect(&rc);
+
+    CDC mem; mem.CreateCompatibleDC(&dc);
+    CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
+    HGDIOBJ ob = mem.SelectObject(bmp);
+
+    PaintTo(&mem);
+    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &mem, 0, 0, SRCCOPY);
+
+    mem.SelectObject(ob);
+}
+
+void CCarrierSlotGrid::OnSize(UINT nType, int cx, int cy)
+{
+    CWnd::OnSize(nType, cx, cy);
+    UpdateScrollRange();
+    Invalidate(FALSE);
+    RedrawWindow(nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW);
+}
+
+void CCarrierSlotGrid::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
+{
+    if (m_noScrollbars) return; // 鈫� 鏂板
+
+    UNREFERENCED_PARAMETER(pBar);
+
+    SCROLLINFO si = { sizeof(SCROLLINFO) };
+    si.fMask = SIF_ALL;
+    GetScrollInfo(SB_HORZ, &si);
+
+    int pos = m_scrollX;
+    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
+
+    switch (nSBCode)
+    {
+    case SB_LINELEFT:  pos -= 30; break;
+    case SB_LINERIGHT: pos += 30; break;
+    case SB_PAGELEFT:  pos -= (int)si.nPage; break;
+    case SB_PAGERIGHT: pos += (int)si.nPage; break;
+    case SB_THUMBTRACK:
+    case SB_THUMBPOSITION:
+        pos = (int)si.nTrackPos;   // 鈽� 32 浣嶆嫋鍔ㄤ綅缃�
+        break;
+    default:
+        return;
+    }
+
+    pos = max(0, min(pos, maxPos));
+    if (pos != m_scrollX) {
+        m_scrollX = pos;
+        SetScrollPos(SB_HORZ, m_scrollX);
+        Invalidate(FALSE);
+    }
+}
+
+void CCarrierSlotGrid::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
+{
+    if (m_noScrollbars) return; // 鈫� 鏂板
+
+    UNREFERENCED_PARAMETER(pBar);
+
+    SCROLLINFO si = { sizeof(SCROLLINFO) };
+    si.fMask = SIF_ALL;
+    GetScrollInfo(SB_VERT, &si);
+
+    int pos = m_scrollY;
+    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
+
+    switch (nSBCode)
+    {
+    case SB_LINEUP:    pos -= m_rowHeight; break;
+    case SB_LINEDOWN:  pos += m_rowHeight; break;
+    case SB_PAGEUP:    pos -= (int)si.nPage; break;
+    case SB_PAGEDOWN:  pos += (int)si.nPage; break;
+    case SB_THUMBTRACK:
+    case SB_THUMBPOSITION:
+        pos = (int)si.nTrackPos;   // 鈽� 32 浣嶆嫋鍔ㄤ綅缃�
+        break;
+    default:
+        return;
+    }
+
+    pos = max(0, min(pos, maxPos));
+    if (pos != m_scrollY) {
+        m_scrollY = pos;
+        SetScrollPos(SB_VERT, m_scrollY);
+        Invalidate(FALSE);
+    }
+}
+
+BOOL CCarrierSlotGrid::OnMouseWheel(UINT, short zDelta, CPoint)
+{
+    if (m_noScrollbars) return FALSE; // 鈫� 鏂板锛氬交搴曚笉婊�
+
+    int delta = (zDelta > 0 ? -1 : +1) * (m_rowHeight * 3);
+    m_scrollY = max(0, m_scrollY + delta);
+    UpdateScrollRange();
+    Invalidate(FALSE);
+    return TRUE;
+}
+
+void CCarrierSlotGrid::OnShowWindow(BOOL bShow, UINT nStatus)
+{
+    CWnd::OnShowWindow(bShow, nStatus);
+    if (bShow) { UpdateScrollRange(); Invalidate(FALSE); }
+}
+
+void CCarrierSlotGrid::OnWindowPosChanged(WINDOWPOS* wp)
+{
+    CWnd::OnWindowPosChanged(wp);
+    if (wp && (wp->flags & SWP_SHOWWINDOW)) { UpdateScrollRange(); Invalidate(FALSE); }
+}
+
+void CCarrierSlotGrid::OnLButtonDown(UINT nFlags, CPoint pt)
+{
+    // 鏄惁鎷栧姩鍒楀
+    int edge = m_bAllowResize ? HitHeaderEdge(pt) : -1;  // 鈫� 淇敼
+    if (edge >= 0)
+    {
+        m_bResizing = true;
+        m_resizeEdge = edge;
+        m_resizeStartX = pt.x;
+        m_slotColCXStart = m_slotColCX;
+        m_portColCXsStart = m_portColCXs;
+        SetCapture();
+        return;
+    }
+
+    // Header 鐐瑰嚮锛堜粎澶嶉�夋鍖哄煙锛�
+    if (GetHeaderRect().PtInRect(pt))
+    {
+        for (int i = 1; i <= GetPortCount(); ++i)
+        {
+            CRect rItem = GetHeaderItemRect(i);
+            if (!rItem.PtInRect(pt)) continue;
+
+            CRect rcCb = GetHeaderCheckboxRect(i);
+            if (!rcCb.PtInRect(pt)) return;
+
+            int port = i - 1;
+            if (!SAFE_PORT(port) || m_ports[port].allocated) return;
+
+            bool any = false, all = true;
+            for (auto& cell : m_ports[port].slots) {
+                if (!cell.hasGlass) continue;
+                any = true; if (!cell.checked) { all = false; break; }
+            }
+            if (!any) return;
+            CheckAllInPort(port, all ? FALSE : TRUE, TRUE);
+            return;
+        }
+        return;
+    }
+
+    // Cell 鐐瑰嚮
+    CRect cli = GetClientRectNoSB();
+    if (pt.y < cli.top + m_headerCY) return;
+    int yIn = pt.y - (cli.top + m_headerCY) + m_scrollY;
+    int row = yIn / m_rowHeight;
+    if (!SAFE_SLOT(row)) return;
+
+    int x = pt.x + m_scrollX - cli.left;
+    int sub = 0;
+    if (x < m_slotColCX) sub = 0;
+    else {
+        x -= m_slotColCX;
+        sub = 1;
+        for (size_t i = 0; i < m_portColCXs.size(); ++i) {
+            if (x < m_portColCXs[i]) { sub = (int)i + 1; break; }
+            x -= m_portColCXs[i];
+            sub = (int)i + 2;
+        }
+        if (sub < 1 || sub > GetPortCount()) return;
+    }
+
+    if (sub == 0) return;
+    int port = sub - 1;
+    if (!SAFE_PORT(port)) return;
+
+    auto& pc = m_ports[port];
+    auto& cell = pc.slots[row];
+
+    CRect rc; if (!GetCellRect(row, sub, rc)) return;
+
+    if (pc.allocated || !cell.hasGlass) return;
+
+    if (GetCheckboxRect(rc).PtInRect(pt))
+    {
+        cell.checked = !cell.checked;
+        NotifySelectionChanged(port, row, cell.checked);
+        Invalidate(FALSE);
+        return;
+    }
+
+    if (m_bShowMatToggle && GetMaterialTagRect(rc).PtInRect(pt))
+    {
+        cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
+        NotifyMaterialChanged(port, row, cell.material);
+        Invalidate(FALSE);
+        return;
+    }
+
+    CWnd::OnLButtonDown(nFlags, pt);
+}
+
+void CCarrierSlotGrid::OnLButtonUp(UINT nFlags, CPoint pt)
+{
+    if (m_bResizing)
+    {
+        ReleaseCapture();
+        m_bResizing = false;
+        m_resizeEdge = -1;
+        UpdateScrollRange();
+        Invalidate(FALSE);
+    }
+    CWnd::OnLButtonUp(nFlags, pt);
+}
+
+void CCarrierSlotGrid::OnMouseMove(UINT nFlags, CPoint pt)
+{
+    if (m_bResizing)
+    {
+        int dx = pt.x - m_resizeStartX;
+        if (m_resizeEdge == 0)
+        {
+            int nw = max(m_slotColMin, m_slotColCXStart + dx);
+            if (nw != m_slotColCX) { m_slotColCX = nw; UpdateScrollRange(); Invalidate(FALSE); }
+        }
+        else
+        {
+            int idx = m_resizeEdge - 1; // 璋冩暣 Port idx 鐨勫搴�
+            int nw = max(m_portColMin, m_portColCXsStart[idx] + dx);
+            if (nw != m_portColCXs[idx]) { m_portColCXs[idx] = nw; UpdateScrollRange(); Invalidate(FALSE); }
+        }
+        return;
+    }
+
+    int edge = HitHeaderEdge(pt);
+    if (edge != m_hitEdgeHover)
+    {
+        m_hitEdgeHover = edge;
+        if (m_hitEdgeHover >= 0) ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
+        else ::SetCursor(::LoadCursor(nullptr, IDC_ARROW));
+    }
+}
+
+BOOL CCarrierSlotGrid::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
+{
+    CPoint pt; ::GetCursorPos(&pt); ScreenToClient(&pt);
+    if (m_bAllowResize && (m_bResizing || HitHeaderEdge(pt) >= 0))
+    {
+        ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
+        return TRUE;
+    }
+    return CWnd::OnSetCursor(pWnd, nHitTest, message);
+}
+
+void CCarrierSlotGrid::NotifySelectionChanged(int port, int slot, BOOL checked)
+{
+    // 鍏煎鏃х殑 WM_COMMAND锛堝彲鐣欙紝涔熷彲娉ㄩ噴鎺夛級
+    if (GetParent()) {
+        const int code = 0x2001;
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+    // 鏂扮殑 WM_NOTIFY锛屽甫涓婄储寮曚笌鐘舵��
+    if (GetParent()) {
+        CSG_SEL_CHANGE nm{};
+        nm.hdr.hwndFrom = m_hWnd;
+        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
+        nm.hdr.code = CSGN_SEL_CHANGED;
+        nm.port = port;
+        nm.slot = slot;
+        nm.checked = checked;
+        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
+    }
+}
+
+void CCarrierSlotGrid::NotifyMaterialChanged(int port, int slot, int material)
+{
+    if (GetParent()) {
+        const int code = 0x2002;
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+    if (GetParent()) {
+        CSG_MAT_CHANGE nm{};
+        nm.hdr.hwndFrom = m_hWnd;
+        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
+        nm.hdr.code = CSGN_MAT_CHANGED;
+        nm.port = port;
+        nm.slot = slot;
+        nm.material = material;
+        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
+    }
+}
+
diff --git a/SourceCode/Bond/Servo/CCarrierSlotGrid.h b/SourceCode/Bond/Servo/CCarrierSlotGrid.h
new file mode 100644
index 0000000..b3e2c2e
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotGrid.h
@@ -0,0 +1,195 @@
+#include "stdafx.h"
+#pragma once
+
+#include <vector>
+
+#ifndef _AFX
+#include <afxwin.h>
+#endif
+
+
+// 放到 CCarrierSlotGrid 类定义前或内部 public: 区都可
+enum { CSGN_SEL_CHANGED = 1, CSGN_MAT_CHANGED = 2 };
+
+struct CSG_SEL_CHANGE {
+    NMHDR hdr;   // hdr.code = CSGN_SEL_CHANGED
+    int   port;  // 0..GetPortCount()-1
+    int   slot;  // 0..GetSlotCount()-1
+    BOOL  checked;
+};
+
+struct CSG_MAT_CHANGE {
+    NMHDR hdr;   // hdr.code = CSGN_MAT_CHANGED
+    int   port;
+    int   slot;
+    int   material; // 1=G1, 2=G2
+};
+
+
+class CCarrierSlotGrid : public CWnd
+{
+public:
+    enum MaterialType { MAT_G1 = 1, MAT_G2 = 2 };
+
+    struct SlotCell {
+        bool    hasGlass = false;
+        CString coreId;
+        int     material = MAT_G1;
+        bool    checked = false;
+    };
+    struct PortColumn {
+        CString               portName;
+        CString               carrierName;
+        bool                  allocated = false;
+        CString               allocatedBy;
+        std::vector<SlotCell> slots;
+    };
+
+public:
+    CCarrierSlotGrid();
+    virtual ~CCarrierSlotGrid();
+
+    BOOL SubclassDlgItem(UINT nID, CWnd* pParent) {
+        BOOL ok = CWnd::SubclassDlgItem(nID, pParent);
+        if (ok) {
+            if (pParent && pParent->GetFont()) SetFont(pParent->GetFont());
+            EnsureFonts();
+            Invalidate(FALSE);
+        }
+        return ok;
+    }
+    virtual void PreSubclassWindow() override;
+
+    // 初始化
+    void InitGrid(int nPorts, int nSlots);
+    void SetColumnWidths(int slotColWidth, int portColWidth);
+    void SetRowHeight(int cy);
+    void SetHeaderHeight(int cy);
+    void SetShowMaterialToggle(BOOL bShow);
+
+    // 读/写
+    int  GetPortCount() const { return (int)m_ports.size(); }
+    int  GetSlotCount() const { return m_nSlots; }
+
+    void SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName);
+    void SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName = nullptr);
+    BOOL IsPortAllocated(int portIndex) const;
+
+    void SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material);
+    void SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify = FALSE);
+    BOOL GetSlotChecked(int portIndex, int slotIndex) const;
+
+    int  GetSlotMaterialType(int portIndex, int slotIndex) const;
+    void SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify = TRUE);
+
+    CString GetDisplayId(int portIndex, int slotIndex) const;
+    void    CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify = TRUE);
+
+    void RebuildTexts();
+    void EnableColumnResize(BOOL enable) { m_bAllowResize = !!enable; Invalidate(FALSE); }
+
+    // 计算最佳大小:
+// - CalcBestClientSize:内容区域(不含滚动条/非客户区)刚好容纳表头+所有行、全部列
+// - CalcBestWindowSize:在当前窗口样式下,将“内容大小”转换为窗口外框大小(会考虑 WS_BORDER/CLIENTEDGE 等)
+//   默认按“隐藏滚动条”的目标来算(即不把 WS_HSCROLL/WS_VSCROLL 计入调整)
+// 计算最佳大小(支持可选安全边距,默认按 DPI 约等于 2px)
+    CSize CalcBestClientSize(int nSlotsOverride = -1) const;
+    CSize CalcBestWindowSize(BOOL includeNonClient = TRUE,
+        int nSlotsOverride = -1,
+        int extraPadX = -1,  // -1 表示按 DPI 自动
+        int extraPadY = -1) const;
+
+    // 永久禁用系统滚动条(去掉样式并刷新非客户区)
+    void DisableSystemScrollbars();
+
+    // 把窗口尺寸调到正好容纳所有内容(不出现滚动条)
+    void ResizeWindowToFitAll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
+
+    // 进入/退出无滚动条模式(去样式、清滚动、忽略滚动消息)
+    void SetNoScrollbarsMode(BOOL enable);
+
+    // 在“无滚动条模式”下,把窗口尺寸调到刚好容纳所有内容(不出现滚动条)
+    void FitWindowToContentNoScroll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
+
+protected:
+    // 数据
+    int                     m_nSlots = 0;
+    std::vector<PortColumn> m_ports;
+    BOOL                    m_bShowMatToggle = TRUE;
+
+    // 尺寸/滚动
+    int m_rowHeight = 26;
+    int m_headerCY = 28;
+    int m_slotColCX = 100;
+    std::vector<int> m_portColCXs;
+    int m_scrollY = 0;
+    int m_scrollX = 0;
+    int m_slotColMin = 60;
+    int m_portColMin = 80;
+
+    // 颜色
+    COLORREF m_colBg = RGB(255, 255, 255);
+    COLORREF m_colAlt = RGB(240, 242, 245);
+    COLORREF m_colLock = RGB(255, 244, 214);
+    COLORREF m_gridMajor = RGB(210, 214, 220);
+    COLORREF m_gridMinor = RGB(220, 224, 230);
+    COLORREF m_text = RGB(40, 40, 40);
+    COLORREF m_textDim = RGB(150, 150, 150);
+
+    // 字体
+    CFont m_fntText;
+    CFont m_fntBold;
+    CFont m_fntSmall;
+
+    // 拖动列宽
+    bool m_bResizing = false;
+    int  m_resizeEdge = -1;   // 0=Slot|Port1;1..N-1=Port i|i+1
+    int  m_resizeStartX = 0;
+    int  m_slotColCXStart = 0;
+    std::vector<int> m_portColCXsStart;
+    int  m_hitEdgeHover = -1;
+    bool m_bAllowResize = true; // ← 新增:是否允许拖动列宽
+    bool m_noScrollbars = false;   // ← 新增:无滚动条模式
+
+    // 工具
+    void EnsureFonts();
+    void UpdateScrollRange();
+    int  GetTotalContentWidth() const;
+    void NotifySelectionChanged(int port, int slot, BOOL checked);
+    void NotifyMaterialChanged(int port, int slot, int material);
+
+    // 几何
+    CRect GetClientRectNoSB() const;
+    BOOL  GetCellRect(int row, int sub, CRect& rc) const; // sub: 0=Slot, 1..N=Port
+    CRect GetHeaderRect() const;
+    CRect GetHeaderItemRect(int iItem) const;
+    CRect GetHeaderCheckboxRect(int iItem) const;
+    CRect GetCheckboxRect(const CRect& cell) const;
+    CRect GetMaterialTagRect(const CRect& cell) const;
+    CRect GetStatusDotRect(const CRect& cell) const;
+    BOOL IsColumnResizeEnabled() const { return m_bAllowResize ? TRUE : FALSE; }
+
+    int   HitHeaderEdge(CPoint pt) const;
+
+    // 绘制
+    void DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled);
+    void PaintTo(CDC* pDC);
+
+protected:
+    // 消息
+    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
+    afx_msg void OnPaint();
+    afx_msg int  OnCreate(LPCREATESTRUCT lpCreateStruct);
+    afx_msg void OnSize(UINT nType, int cx, int cy);
+    afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
+    afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); // ★ 纵向滚动条
+    afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
+    afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
+    afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
+    afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
+    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
+    afx_msg void OnShowWindow(BOOL bShow, UINT nStatus);
+    afx_msg void OnWindowPosChanged(WINDOWPOS* wp);
+
+    DECLARE_MESSAGE_MAP()
+};
diff --git a/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp b/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp
new file mode 100644
index 0000000..20be4d4
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotSelector.cpp
@@ -0,0 +1,673 @@
+#include "stdafx.h"
+#include "CCarrierSlotSelector.h"
+
+#define SAFE_PORT(p)    ((p) >= 0 && (p) < (int)m_ports.size())
+#define SAFE_SLOT(s)    ((s) >= 0 && (s) < m_nSlots)
+
+#ifndef LVS_EX_DOUBLEBUFFER
+#define LVS_EX_DOUBLEBUFFER 0x00010000
+#endif
+
+BEGIN_MESSAGE_MAP(CCarrierSlotSelector, CListCtrl)
+    ON_WM_SHOWWINDOW()
+    ON_WM_WINDOWPOSCHANGED()
+    ON_WM_SIZE()
+    ON_WM_ERASEBKGND()
+    ON_WM_PAINT()                           // ★ 新增
+    ON_WM_LBUTTONDOWN()
+    ON_WM_MOUSEMOVE()
+    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CCarrierSlotSelector::OnCustomDraw)
+END_MESSAGE_MAP()
+
+CCarrierSlotSelector::CCarrierSlotSelector() {}
+CCarrierSlotSelector::~CCarrierSlotSelector()
+{
+    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
+    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
+    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
+    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
+}
+
+void CCarrierSlotSelector::PreSubclassWindow()
+{
+    CListCtrl::PreSubclassWindow();
+
+    ModifyStyle(LVS_TYPEMASK, LVS_REPORT | LVS_SHOWSELALWAYS);
+    ModifyStyle(LVS_OWNERDRAWFIXED | LVS_OWNERDATA, 0);
+
+    DWORD ex = GetExtendedStyle();
+    ex |= LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER;
+    ex &= ~LVS_EX_GRIDLINES; // 关闭系统网格,改自绘
+    SetExtendedStyle(ex);
+
+    // 让默认绘制用我们的底色,进一步降低白底机会(即使哪处走了默认路径)
+    ListView_SetBkColor(m_hWnd, m_colBgNorm);
+    ListView_SetTextBkColor(m_hWnd, m_colBgNorm);
+
+    EnsureFonts();
+    SetRowHeight(m_rowHeight);
+}
+
+void CCarrierSlotSelector::OnShowWindow(BOOL bShow, UINT nStatus)
+{
+    CListCtrl::OnShowWindow(bShow, nStatus);
+    if (bShow && !m_bFirstShown)
+    {
+        m_bFirstShown = TRUE;
+        RedrawWindow(nullptr, nullptr,
+            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
+    }
+}
+
+void CCarrierSlotSelector::OnWindowPosChanged(WINDOWPOS* wp)
+{
+    CListCtrl::OnWindowPosChanged(wp);
+    if (wp && (wp->flags & SWP_SHOWWINDOW))
+    {
+        RedrawWindow(nullptr, nullptr,
+            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
+    }
+}
+
+void CCarrierSlotSelector::EnsureFonts()
+{
+    if (!(HFONT)m_fntText)
+    {
+        LOGFONT lf = { 0 };
+        CFont* pSys = GetFont();
+        if (pSys) pSys->GetLogFont(&lf);
+        else { lf.lfHeight = -14; lstrcpy(lf.lfFaceName, _T("Segoe UI")); }
+        m_fntText.CreateFontIndirect(&lf);
+
+        lf.lfWeight = FW_SEMIBOLD;
+        m_fntBold.CreateFontIndirect(&lf);
+
+        lf.lfWeight = FW_NORMAL; lf.lfHeight = -12;
+        m_fntSmall.CreateFontIndirect(&lf);
+    }
+}
+
+void CCarrierSlotSelector::InitGrid(int nPorts, int nSlots)
+{
+    ASSERT(nPorts >= 1 && nSlots >= 1);
+    m_ports.clear();
+    m_ports.resize(nPorts);
+    m_nSlots = nSlots;
+    for (auto& pc : m_ports) pc.slots.resize(m_nSlots);
+
+    SetRedraw(FALSE);
+    DeleteAllItems();
+    while (GetHeaderCtrl() && GetHeaderCtrl()->GetItemCount() > 0)
+        DeleteColumn(0);
+
+    InsertColumn(0, _T("Slot"), LVCFMT_LEFT, m_slotColWidth);
+    for (int c = 0; c < nPorts; ++c)
+    {
+        CString col; col.Format(_T("Port %d"), c + 1);
+        InsertColumn(c + 1, col, LVCFMT_LEFT, m_portColWidth);
+        m_ports[c].portName = col;
+        m_ports[c].carrierName.Empty();
+    }
+
+    UpdateRowCount();
+    RebuildTexts();
+    SetRedraw(TRUE);
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetColumnWidths(int slotColWidth, int portColWidth)
+{
+    if (slotColWidth > 0) m_slotColWidth = slotColWidth;
+    if (portColWidth > 0) m_portColWidth = portColWidth;
+    SetColumnWidth(0, m_slotColWidth);
+    for (int c = 0; c < (int)m_ports.size(); ++c) SetColumnWidth(c + 1, m_portColWidth);
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetRowHeight(int cy)
+{
+    cy = max(18, min(cy, 64));
+    m_rowHeight = cy;
+
+    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
+    m_ilRowHeight.Create(1, m_rowHeight, ILC_COLOR32, 1, 1);
+
+    // 1×cy 透明位图
+    CBitmap bmp; bmp.CreateBitmap(1, m_rowHeight, 1, 32, nullptr);
+    m_ilRowHeight.Add(&bmp, RGB(0, 0, 0));
+    SetImageList(&m_ilRowHeight, LVSIL_SMALL);
+
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::UpdateRowCount()
+{
+    int cur = GetItemCount();
+    if (cur < m_nSlots)
+    {
+        for (int i = cur; i < m_nSlots; ++i)
+        {
+            CString s; s.Format(_T("Slot %d"), i + 1);
+            InsertItem(i, s, 0); // 设 iImage=0,行高来自 small image list
+        }
+    }
+    else if (cur > m_nSlots)
+    {
+        for (int i = cur - 1; i >= m_nSlots; --i) DeleteItem(i);
+    }
+
+    // 已有行也统一设 iImage=0
+    LVITEM lvi = { 0 };
+    lvi.mask = LVIF_IMAGE; lvi.iImage = 0;
+    for (int i = 0; i < m_nSlots; ++i) { lvi.iItem = i; SetItem(&lvi); }
+}
+
+void CCarrierSlotSelector::RebuildTexts()
+{
+    // 列头:显示 “已选数/总槽数 + [x]/[ ]”
+    for (int c = 0; c < (int)m_ports.size(); ++c)
+    {
+        int selected = 0;
+        int total = m_nSlots;
+
+        bool anyCheckable = false;
+        bool allChecked = true;
+
+        for (int r = 0; r < m_nSlots; ++r)
+        {
+            const auto& cell = m_ports[c].slots[r];
+            if (cell.hasGlass)
+            {
+                anyCheckable = true;
+                if (cell.checked) ++selected; else allChecked = false;
+            }
+        }
+        if (!anyCheckable) allChecked = false;
+
+        CString head;
+        CString chk = allChecked ? _T("[x]") : _T("[ ]");
+
+        if (!m_ports[c].carrierName.IsEmpty())
+            head.Format(_T("%s (%s)   %d/%d  %s%s"),
+                m_ports[c].portName.GetString(), m_ports[c].carrierName.GetString(),
+                selected, total,
+                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
+                chk.GetString());
+        else
+            head.Format(_T("%s   %d/%d  %s%s"),
+                m_ports[c].portName.GetString(), selected, total,
+                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
+                chk.GetString());
+
+        LVCOLUMN lvc = { 0 }; lvc.mask = LVCF_TEXT; lvc.pszText = head.GetBuffer();
+        SetColumn(c + 1, &lvc);
+        head.ReleaseBuffer();
+    }
+
+    // 行头文本
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        CString s; s.Format(_T("Slot %d"), r + 1);
+        SetItemText(r, 0, s);
+    }
+
+    // 单元格临时文本
+    for (int r = 0; r < m_nSlots; ++r)
+        for (int c = 0; c < (int)m_ports.size(); ++c)
+            SetItemText(r, c + 1, GetDisplayId(c, r));
+}
+
+void CCarrierSlotSelector::SetShowMaterialToggle(BOOL bShow)
+{
+    m_bShowMatToggle = bShow;
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    if (portName)    m_ports[portIndex].portName = portName;
+    if (carrierName) m_ports[portIndex].carrierName = carrierName;
+    RebuildTexts();
+    if (IsWindowVisible()) Invalidate(FALSE);
+}
+
+void CCarrierSlotSelector::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    m_ports[portIndex].allocated = !!allocated;
+    if (byName) m_ports[portIndex].allocatedBy = byName; else m_ports[portIndex].allocatedBy.Empty();
+
+    if (allocated)
+        for (int r = 0; r < m_nSlots; ++r) m_ports[portIndex].slots[r].checked = false;
+
+    // 刷新整列
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::IsPortAllocated(int portIndex) const
+{
+    if (!SAFE_PORT(portIndex)) return FALSE;
+    return m_ports[portIndex].allocated;
+}
+
+void CCarrierSlotSelector::SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    cell.hasGlass = !!hasGlass;
+    cell.coreId = coreId ? coreId : _T("");
+    cell.material = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (!cell.hasGlass) cell.checked = false;
+
+    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+    InvalidateRect(&rc, FALSE);
+    RebuildTexts();
+}
+
+void CCarrierSlotSelector::SetSlotChecked(int portIndex, int slotIndex, BOOL checked)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return;
+
+    cell.checked = !!checked;
+    NotifySelectionChanged(portIndex, slotIndex, cell.checked);
+
+    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+    InvalidateRect(&rc, FALSE);
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::GetSlotChecked(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return FALSE;
+    return m_ports[portIndex].slots[slotIndex].checked ? TRUE : FALSE;
+}
+
+int CCarrierSlotSelector::GetSlotMaterialType(int portIndex, int slotIndex) const
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
+    return m_ports[portIndex].slots[slotIndex].material;
+}
+
+void CCarrierSlotSelector::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+    auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return;
+
+    int mt = (material == MAT_G2) ? MAT_G2 : MAT_G1;
+    if (cell.material != mt)
+    {
+        cell.material = mt;
+        if (bNotify) NotifyMaterialChanged(portIndex, slotIndex, cell.material);
+        CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+}
+
+CString CCarrierSlotSelector::GetDisplayId(int portIndex, int slotIndex) const
+{
+    CString s(_T("—"));
+    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return s;
+    const auto& cell = m_ports[portIndex].slots[slotIndex];
+    if (!cell.hasGlass) return s;
+    s.Format(_T("G%d-%s"), (cell.material == MAT_G2) ? 2 : 1, cell.coreId.GetString());
+    return s;
+}
+
+void CCarrierSlotSelector::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
+{
+    if (!SAFE_PORT(portIndex)) return;
+    if (m_ports[portIndex].allocated) return;
+
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        auto& cell = m_ports[portIndex].slots[r];
+        if (!cell.hasGlass) continue;
+        if (cell.checked != !!checked)
+        {
+            cell.checked = !!checked;
+            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
+        }
+        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
+        InvalidateRect(&rc, FALSE);
+    }
+    RebuildTexts();
+}
+
+BOOL CCarrierSlotSelector::OnEraseBkgnd(CDC* /*pDC*/)
+{
+    // 关键:不让系统擦白背景;用自绘在 PREPAINT 阶段统一铺底
+    return TRUE;
+}
+
+void CCarrierSlotSelector::OnPaint()
+{
+    CPaintDC dc(this);
+    CRect rc; GetClientRect(&rc);
+
+    CDC memDC;
+    memDC.CreateCompatibleDC(&dc);
+    CBitmap bmp;
+    bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
+    HGDIOBJ hOldBmp = memDC.SelectObject(bmp);
+
+    // 统一底色(避免任何擦白)
+    memDC.FillSolidRect(rc, m_colBgNorm);
+
+    // 让控件把客户区内容“画到”内存DC(会触发 NM_CUSTOMDRAW,走你现有自绘逻辑)
+    // PRF_ERASEBKGND 让内部如果想擦背景,也在内存里擦,不会闪屏
+    SendMessage(WM_PRINTCLIENT,
+        reinterpret_cast<WPARAM>(memDC.m_hDC),
+        PRF_CLIENT | PRF_ERASEBKGND | PRF_CHILDREN | PRF_OWNED);
+
+    // 回显到屏幕
+    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY);
+
+    memDC.SelectObject(hOldBmp);
+    // bmp, memDC 析构自动释放
+}
+
+void CCarrierSlotSelector::OnSize(UINT nType, int cx, int cy)
+{
+    CListCtrl::OnSize(nType, cx, cy);
+}
+
+CRect CCarrierSlotSelector::GetCheckboxRect(const CRect& cell) const
+{
+    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
+    int leftPad = 8;
+    int top = cell.top + (m_rowHeight - sz) / 2;
+    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
+}
+
+CRect CCarrierSlotSelector::GetMaterialTagRect(const CRect& cell) const
+{
+    int tagH = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
+    int tagW = 32;
+    int rightPadForDot = 16;
+    int gap = 6;
+    int top = cell.top + (m_rowHeight - tagH) / 2;
+    int right = cell.right - rightPadForDot - gap;
+    return CRect(right - tagW, top, right, top + tagH);
+}
+
+CRect CCarrierSlotSelector::GetStatusDotRect(const CRect& cell) const
+{
+    int d = max(8, min(int(m_rowHeight * 0.42), 12));
+    int rightPad = 6;
+    int top = cell.top + (m_rowHeight - d) / 2;
+    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
+}
+
+void CCarrierSlotSelector::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
+{
+    CBrush br(disabled ? RGB(245, 245, 245) : RGB(255, 255, 255));
+    CPen   pen(PS_SOLID, 1, disabled ? RGB(200, 200, 200) : RGB(130, 130, 135));
+    CBrush* pOldB = pDC->SelectObject(&br);
+    CPen* pOldP = pDC->SelectObject(&pen);
+    pDC->RoundRect(r, CPoint(3, 3));
+    pDC->SelectObject(pOldB);
+    pDC->SelectObject(pOldP);
+    br.DeleteObject(); pen.DeleteObject();
+
+    if (!checked) return;
+
+    COLORREF c = disabled ? RGB(160, 160, 160) : RGB(40, 150, 90);
+    CPen penTick(PS_SOLID, max(2, r.Height() / 8), c);
+    CPen* pOld = pDC->SelectObject(&penTick);
+    POINT p1 = { r.left + r.Width() * 2 / 9, r.top + r.Height() * 5 / 9 };
+    POINT p2 = { r.left + r.Width() * 4 / 9, r.top + r.Height() * 7 / 9 };
+    POINT p3 = { r.left + r.Width() * 7 / 9, r.top + r.Height() * 3 / 9 };
+    pDC->MoveTo(p1); pDC->LineTo(p2); pDC->LineTo(p3);
+    pDC->SelectObject(pOld);
+    penTick.DeleteObject();
+}
+
+void CCarrierSlotSelector::OnLButtonDown(UINT nFlags, CPoint point)
+{
+    LVHITTESTINFO ht = { 0 }; ht.pt = point;
+    int row = SubItemHitTest(&ht);
+    int sub = (row >= 0) ? ht.iSubItem : -1;
+
+    if (row >= 0 && sub >= 1)
+    {
+        int port = sub - 1;
+        if (SAFE_PORT(port) && SAFE_SLOT(row))
+        {
+            auto& pc = m_ports[port];
+            auto& cell = pc.slots[row];
+            CRect rcCell; GetSubItemRect(row, sub, LVIR_BOUNDS, rcCell);
+
+            if (pc.allocated || !cell.hasGlass)
+            {
+                CListCtrl::OnLButtonDown(nFlags, point);
+                return;
+            }
+
+            if (GetCheckboxRect(rcCell).PtInRect(point))
+            {
+                cell.checked = !cell.checked;
+                NotifySelectionChanged(port, row, cell.checked);
+                InvalidateRect(&rcCell, FALSE);
+                RebuildTexts();
+                return;
+            }
+
+            if (m_bShowMatToggle && GetMaterialTagRect(rcCell).PtInRect(point))
+            {
+                cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
+                NotifyMaterialChanged(port, row, cell.material);
+                InvalidateRect(&rcCell, FALSE);
+                return;
+            }
+        }
+    }
+
+    CListCtrl::OnLButtonDown(nFlags, point);
+}
+
+void CCarrierSlotSelector::OnMouseMove(UINT nFlags, CPoint point)
+{
+    CListCtrl::OnMouseMove(nFlags, point);
+}
+
+void CCarrierSlotSelector::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    NMLVCUSTOMDRAW* pCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR);
+
+    switch (pCD->nmcd.dwDrawStage)
+    {
+    case CDDS_PREPAINT:
+    {
+        // 关键:整控件预先铺底,避免初次 hover/显示时“刷白”
+        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
+        CRect rcClient; GetClientRect(&rcClient);
+        pDC->FillSolidRect(rcClient, m_colBgNorm);
+        *pResult = CDRF_NOTIFYITEMDRAW;
+        return;
+    }
+
+    case CDDS_ITEMPREPAINT:
+        *pResult = CDRF_NOTIFYSUBITEMDRAW;
+        return;
+
+    case (CDDS_SUBITEM | CDDS_ITEMPREPAINT):
+    {
+        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
+        int row = (int)pCD->nmcd.dwItemSpec;
+        int sub = pCD->iSubItem;
+
+        CRect rc; GetSubItemRect(row, sub, LVIR_BOUNDS, rc);
+
+        // 背景
+        COLORREF bk = m_colBgNorm;
+        if (sub >= 1)
+        {
+            int port = sub - 1;
+            if (port % 2 == 0) bk = m_colBgAlt;             // 交替列
+            if (SAFE_PORT(port) && m_ports[port].allocated) // 锁定列
+                bk = m_colLockBg;
+        }
+        pDC->FillSolidRect(rc, bk);
+
+        // 网格(上/左加粗;右/下细线)
+        CPen penTopLeft(PS_SOLID, (sub >= 1 && ((sub - 1) % 2 == 0)) ? 2 : 1, RGB(210, 214, 220));
+        CPen* pOldPen = pDC->SelectObject(&penTopLeft);
+        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.right, rc.top);
+        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.left, rc.bottom);
+        pDC->SelectObject(pOldPen); penTopLeft.DeleteObject();
+
+        CPen penThin(PS_SOLID, 1, RGB(220, 224, 230));
+        pOldPen = pDC->SelectObject(&penThin);
+        if (sub == GetHeaderCtrl()->GetItemCount() - 1) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
+        if (row == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
+        pDC->SelectObject(pOldPen); penThin.DeleteObject();
+
+        if (sub == 0)
+        {
+            CString s; s.Format(_T("Slot %d"), row + 1);
+            CFont* pOld = pDC->SelectObject(&m_fntBold);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SetTextColor(RGB(60, 60, 64));
+            CRect rText = rc; rText.DeflateRect(8, 0, 8, 0);
+            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
+            pDC->SelectObject(pOld);
+        }
+        else if (SAFE_PORT(sub - 1) && SAFE_SLOT(row))
+        {
+            int port = sub - 1;
+            const auto& pc = m_ports[port];
+            const auto& cell = pc.slots[row];
+
+            // 扁平复选框
+            CRect rChk = GetCheckboxRect(rc);
+            DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
+
+            // 文本
+            CString s = GetDisplayId(port, row);
+            CRect rText = rc;
+            int leftPad = rChk.right + 6;
+            CRect rDot = GetStatusDotRect(rc);
+            CRect rTag = GetMaterialTagRect(rc);
+            int rightPad = rc.right - min(rTag.left - 6, rDot.left - 6);
+            rText.DeflateRect(leftPad - rc.left, 0, rightPad, 0);
+
+            CFont* pOld = pDC->SelectObject(&m_fntText);
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SetTextColor(cell.hasGlass ? RGB(40, 40, 40) : RGB(150, 150, 150));
+            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
+            pDC->SelectObject(pOld);
+
+            // 物料标签(可隐藏)
+            if (m_bShowMatToggle)
+            {
+                CRect rT = rTag;
+                COLORREF crBorder = (cell.material == MAT_G2) ? RGB(180, 150, 220) : RGB(120, 160, 220);
+                COLORREF crFill = (cell.material == MAT_G2) ? RGB(243, 235, 250) : RGB(233, 240, 252);
+                COLORREF crText = (cell.material == MAT_G2) ? RGB(90, 60, 150) : RGB(50, 90, 160);
+                if (pc.allocated || !cell.hasGlass) { crBorder = RGB(210, 210, 210); crFill = RGB(245, 245, 245); crText = RGB(160, 160, 160); }
+                CBrush br(crFill); CPen tagPen(PS_SOLID, 1, crBorder);
+                CPen* pOldP = pDC->SelectObject(&tagPen);
+                CBrush* pOldB = pDC->SelectObject(&br);
+                pDC->RoundRect(rT, CPoint(6, 6));
+                pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
+                br.DeleteObject(); tagPen.DeleteObject();
+
+                CString t; t.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
+                CFont* pOldS = pDC->SelectObject(&m_fntSmall);
+                pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
+                pDC->DrawText(t, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
+                pDC->SelectObject(pOldS);
+            }
+
+            // 状态点
+            CRect rD = GetStatusDotRect(rc);
+            COLORREF dot = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
+            CBrush bDot(dot); CBrush* pOldB = pDC->SelectObject(&bDot);
+            pDC->Ellipse(rD);
+            pDC->SelectObject(pOldB); bDot.DeleteObject();
+        }
+
+        *pResult = CDRF_SKIPDEFAULT; // 子项完全自绘
+        return;
+    }
+
+    default:
+        break;
+    }
+
+    *pResult = 0;
+}
+
+BOOL CCarrierSlotSelector::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
+{
+    NMHDR* pNM = reinterpret_cast<NMHDR*>(lParam);
+    if (pNM && pNM->hwndFrom == GetHeaderCtrl()->GetSafeHwnd())
+    {
+        switch (pNM->code)
+        {
+        case HDN_ITEMCLICKA:
+        case HDN_ITEMCLICKW:
+        {
+            HD_NOTIFY* phdn = reinterpret_cast<HD_NOTIFY*>(lParam);
+            OnHeaderClick(phdn->iItem);
+            if (pResult) *pResult = 0;
+            return TRUE; // 我们已处理
+        }
+        default: break;
+        }
+    }
+    return CListCtrl::OnNotify(wParam, lParam, pResult);
+}
+
+void CCarrierSlotSelector::OnHeaderClick(int iItem)
+{
+    // iItem: 0=Slot 列,>=1 为 Port 列
+    if (iItem <= 0) return;
+    int port = iItem - 1;
+    if (!SAFE_PORT(port) || m_ports[port].allocated) return;
+
+    // 计算是否“全选”
+    bool anyCheckable = false;
+    bool allChecked = true;
+    for (int r = 0; r < m_nSlots; ++r)
+    {
+        const auto& cell = m_ports[port].slots[r];
+        if (!cell.hasGlass) continue;
+        anyCheckable = true;
+        if (!cell.checked) { allChecked = false; break; }
+    }
+    if (!anyCheckable) return;
+
+    // 切换:若已全选 -> 取消全选;否则 -> 全选
+    CheckAllInPort(port, allChecked ? FALSE : TRUE, TRUE);
+}
+
+// ==== 通知 ====
+void CCarrierSlotSelector::NotifySelectionChanged(int /*port*/, int /*slot*/, BOOL /*checked*/)
+{
+    if (GetParent())
+    {
+        const int code = 0x2001; // CSSN_SELECTION_CHANGED
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+}
+
+void CCarrierSlotSelector::NotifyMaterialChanged(int /*port*/, int /*slot*/, int /*material*/)
+{
+    if (GetParent())
+    {
+        const int code = 0x2002; // CSSN_MATERIAL_CHANGED
+        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+    }
+}
diff --git a/SourceCode/Bond/Servo/CCarrierSlotSelector.h b/SourceCode/Bond/Servo/CCarrierSlotSelector.h
new file mode 100644
index 0000000..9ecbf97
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCarrierSlotSelector.h
@@ -0,0 +1,122 @@
+#include "stdafx.h"
+#pragma once
+
+#include <vector>
+#include <string>
+
+#ifndef _AFX
+#include <afxwin.h>
+#include <afxcmn.h>
+#endif
+
+class CCarrierSlotSelector : public CListCtrl
+{
+public:
+    enum MaterialType { MAT_G1 = 1, MAT_G2 = 2 };
+
+    struct SlotCell
+    {
+        bool    hasGlass = false;   // 是否有片
+        CString coreId;             // 固定核心ID(不含前缀)
+        int     material = MAT_G1;  // 仅影响展示(G1/G2)
+        bool    checked = false;   // 是否勾选加工
+    };
+
+    struct PortColumn
+    {
+        CString               portName;     // "Port 1" ...
+        CString               carrierName;  // "Carrier A" ...
+        bool                  allocated = false; // 整列锁定
+        CString               allocatedBy;       // 占用者
+        std::vector<SlotCell> slots;             // size = m_nSlots
+    };
+
+public:
+    CCarrierSlotSelector();
+    virtual ~CCarrierSlotSelector();
+
+    // 初始化 / 尺寸
+    void    InitGrid(int nPorts, int nSlots);
+    void    SetColumnWidths(int slotColWidth, int portColWidth);
+    void    SetRowHeight(int cy); // small image list 控制行高
+
+    // 外观
+    void    SetShowMaterialToggle(BOOL bShow);
+    BOOL    GetShowMaterialToggle() const { return m_bShowMatToggle; }
+
+    // Port 接口
+    int     GetPortCount() const { return (int)m_ports.size(); }
+    int     GetSlotCount() const { return m_nSlots; }
+    void    SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName);
+    void    SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName = nullptr);
+    BOOL    IsPortAllocated(int portIndex) const;
+
+    // Slot 接口
+    void    SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId /*可空*/, int material /*1=G1,2=G2*/);
+    void    SetSlotChecked(int portIndex, int slotIndex, BOOL checked);
+    BOOL    GetSlotChecked(int portIndex, int slotIndex) const;
+
+    int     GetSlotMaterialType(int portIndex, int slotIndex) const; // 1/2
+    void    SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify = TRUE);
+
+    CString GetDisplayId(int portIndex, int slotIndex) const; // "Gx-core" 或 "—"
+    void    RebuildTexts(); // 列头计数、单元格临时文本
+
+    // 工具:整列全选/全不选(只影响 hasGlass==true 的格子)
+    void    CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify = TRUE);
+
+protected:
+    // 内部数据
+    BOOL                    m_bFirstShown = FALSE; // 子页面首次显示后强制一次全量重绘
+    int                     m_nSlots = 0;
+    std::vector<PortColumn> m_ports;
+    BOOL                    m_bShowMatToggle = TRUE;
+
+    // UI metrics
+    int     m_rowHeight = 24;
+    int     m_slotColWidth = 100;
+    int     m_portColWidth = 180;
+
+    // 行高图像列表
+    CImageList m_ilRowHeight;
+
+    // 颜色
+    COLORREF m_colBgAlt = RGB(240, 242, 245); // 交替列浅灰
+    COLORREF m_colBgNorm = RGB(255, 255, 255);
+    COLORREF m_colLockBg = RGB(255, 244, 214); // 锁定列淡黄
+
+    // 字体
+    CFont   m_fntText;
+    CFont   m_fntBold;
+    CFont   m_fntSmall;
+
+    // 区域计算
+    CRect   GetCheckboxRect(const CRect& cell) const;
+    CRect   GetMaterialTagRect(const CRect& cell) const;
+    CRect   GetStatusDotRect(const CRect& cell) const;
+
+    // 工具
+    void    EnsureFonts();
+    void    UpdateRowCount();
+    void    DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled); // 扁平复选框
+
+    // 通知父窗口(WM_COMMAND 风格)
+    void    NotifySelectionChanged(int port, int slot, BOOL checked);
+    void    NotifyMaterialChanged(int port, int slot, int material);
+
+protected:
+    // MFC
+    virtual void PreSubclassWindow() override;
+    afx_msg void OnShowWindow(BOOL bShow, UINT nStatus);
+    afx_msg void OnWindowPosChanged(WINDOWPOS* lpwndpos);
+    afx_msg void OnSize(UINT nType, int cx, int cy);
+    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
+    afx_msg void OnPaint();                // ★ 新增:自带双缓冲绘制
+    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
+    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
+    afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
+    afx_msg BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); // 捕获 Header 通知
+    void    OnHeaderClick(int iItem);
+
+    DECLARE_MESSAGE_MAP()
+};
diff --git a/SourceCode/Bond/Servo/CCjPage1.cpp b/SourceCode/Bond/Servo/CCjPage1.cpp
new file mode 100644
index 0000000..cb35ba1
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage1.cpp
@@ -0,0 +1,65 @@
+锘�// CPjPage1.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CCjPage1.h"
+#include "afxdialogex.h"
+
+
+// CPjPage1 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CCjPage1, CCjPageBase)
+
+CCjPage1::CCjPage1(CWnd* pParent /*=nullptr*/)
+	: CCjPageBase(IDD_CJ_PAGE1, pParent)
+{
+}
+
+CCjPage1::~CCjPage1()
+{
+}
+
+void CCjPage1::DoDataExchange(CDataExchange* pDX)
+{
+	CCjPageBase::DoDataExchange(pDX);
+}
+
+
+BEGIN_MESSAGE_MAP(CCjPage1, CCjPageBase)
+	ON_WM_DESTROY()
+END_MESSAGE_MAP()
+
+
+// CPjPage1 娑堟伅澶勭悊绋嬪簭
+
+
+BOOL CCjPage1::OnInitDialog()
+{
+	CCjPageBase::OnInitDialog();
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CCjPage1::OnDestroy()
+{
+	CCjPageBase::OnDestroy();
+
+	// TODO: 鍦ㄦ澶勬坊鍔犳秷鎭鐞嗙▼搴忎唬鐮�
+}
+
+void CCjPage1::Resize()
+{
+	CCjPageBase::Resize();
+	/*
+    CWnd* pItem;
+    CRect rcClient, rcItem;
+    GetClientRect(&rcClient);
+    pItem = GetDlgItem(IDC_LABEL_NO_SEL);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow((rcClient.Width() - rcItem.Width()) / 2,
+        (rcClient.Height() - rcItem.Height()) / 2,
+        rcItem.Width(), rcItem.Height());
+		*/
+}
diff --git a/SourceCode/Bond/Servo/CCjPage1.h b/SourceCode/Bond/Servo/CCjPage1.h
new file mode 100644
index 0000000..3ee0c38
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage1.h
@@ -0,0 +1,31 @@
+锘�#pragma once
+#include "CCjPageBase.h"
+
+
+// CPjPage1 瀵硅瘽妗�
+
+class CCjPage1 : public CCjPageBase
+{
+	DECLARE_DYNAMIC(CCjPage1)
+
+public:
+	CCjPage1(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CCjPage1();
+
+protected:
+	virtual void Resize();
+
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_CJ_PAGE1 };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnDestroy();
+};
diff --git a/SourceCode/Bond/Servo/CCjPage2.cpp b/SourceCode/Bond/Servo/CCjPage2.cpp
new file mode 100644
index 0000000..2f4d559
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage2.cpp
@@ -0,0 +1,480 @@
+锘�// CPjPage1.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CCjPage2.h"
+#include "afxdialogex.h"
+#include "RecipeManager.h"
+
+
+UINT btnID[] = { IDC_BUTTON_PORT1_PROCESS_START,
+    IDC_BUTTON_PORT2_PROCESS_START,
+    IDC_BUTTON_PORT3_PROCESS_START,
+    IDC_BUTTON_PORT4_PROCESS_START };
+
+// CPjPage1 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CCjPage2, CCjPageBase)
+
+CCjPage2::CCjPage2(CWnd* pParent /*=nullptr*/)
+	: CCjPageBase(IDD_CJ_PAGE2, pParent)
+{
+    m_nSelRadioId = 0;
+}
+
+CCjPage2::~CCjPage2()
+{
+}
+
+void CCjPage2::DoDataExchange(CDataExchange* pDX)
+{
+    CCjPageBase::DoDataExchange(pDX);
+}
+
+
+BEGIN_MESSAGE_MAP(CCjPage2, CCjPageBase)
+	ON_WM_DESTROY()
+    ON_EN_CHANGE(IDC_EDIT_PJ_ID, &CCjPage2::OnEnChangeEditPjId)
+    ON_CBN_SELCHANGE(IDC_COMBO_RECIPE, &CCjPage2::OnCbnSelchangeComboRecipe)
+    ON_BN_CLICKED(IDC_RADIO1, &CCjPage2::OnBnClickedRadio1)
+    ON_BN_CLICKED(IDC_RADIO2, &CCjPage2::OnBnClickedRadio2)
+    ON_BN_CLICKED(IDC_RADIO3, &CCjPage2::OnBnClickedRadio3)
+    ON_BN_CLICKED(IDC_RADIO4, &CCjPage2::OnBnClickedRadio4)
+    ON_NOTIFY(CSGN_SEL_CHANGED, IDC_GRID1, &CCjPage2::OnGridSelChanged)
+    ON_NOTIFY(CSGN_MAT_CHANGED, IDC_GRID1, &CCjPage2::OnGridMatChanged)
+    ON_BN_CLICKED(IDC_BUTTON_PORT1_PROCESS_START, &CCjPage2::OnBnClickedButtonPort1ProcessStart)
+    ON_BN_CLICKED(IDC_BUTTON_PORT2_PROCESS_START, &CCjPage2::OnBnClickedButtonPort2ProcessStart)
+    ON_BN_CLICKED(IDC_BUTTON_PORT3_PROCESS_START, &CCjPage2::OnBnClickedButtonPort3ProcessStart)
+    ON_BN_CLICKED(IDC_BUTTON_PORT4_PROCESS_START, &CCjPage2::OnBnClickedButtonPort4ProcessStart)
+END_MESSAGE_MAP()
+
+
+// CPjPage1 娑堟伅澶勭悊绋嬪簭
+
+
+void CCjPage2::OnSetContext(void* pContext)
+{
+    UpdatePjData();
+}
+
+void CCjPage2::SetPjWarps(std::vector<PJWarp>& pjs)
+{
+    m_pjWarps = pjs;
+}
+
+BOOL CCjPage2::OnInitDialog()
+{
+    CCjPageBase::OnInitDialog();
+
+
+    m_grid.SubclassDlgItem(IDC_GRID1, this);
+     m_grid.InitGrid(4, 8);
+    m_grid.SetColumnWidths(100, 220);
+    m_grid.SetRowHeight(32);
+    m_grid.SetHeaderHeight(36);
+    m_grid.EnableColumnResize(FALSE); // 绂佹鎷栧姩鍒楀
+    m_grid.SetShowMaterialToggle(TRUE);
+    m_grid.DisableSystemScrollbars();
+    m_grid.ResizeWindowToFitAll(TRUE); // TRUE=鍖呭惈闈炲鎴峰尯锛堣竟妗嗐�佹爣棰樻爮锛�
+    m_grid.SetNoScrollbarsMode(TRUE);           // 褰诲簳绂佺敤婊氬姩鏉�
+    m_grid.FitWindowToContentNoScroll(TRUE);    // 绐楀彛灏哄鍒氬ソ瀹圭撼鍏ㄩ儴鍐呭锛堜笉鍑虹幇婊氬姩鏉★級
+
+    m_grid.SetPortInfo(0, _T("Port 1"), _T(""));
+    m_grid.SetPortInfo(1, _T("Port 2"), _T(""));
+    m_grid.SetPortInfo(2, _T("Port 3"), _T(""));
+    m_grid.SetPortInfo(3, _T("Port 4"), _T(""));
+
+
+    UpdatePjData();
+
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CCjPage2::OnDestroy()
+{
+    CCjPageBase::OnDestroy();
+
+	// TODO: 鍦ㄦ澶勬坊鍔犳秷鎭鐞嗙▼搴忎唬鐮�
+}
+
+void CCjPage2::Resize()
+{
+    CCjPageBase::Resize();
+ 
+
+    CWnd* pItem;
+    CRect rcClient, rcItem;
+    GetClientRect(&rcClient);
+    pItem = GetDlgItem(IDC_GRID1);
+    pItem->GetWindowRect(&rcItem);
+    ScreenToClient(rcItem);
+
+
+    int x = rcItem.left + 100 + 18;
+    int y = 100;
+
+
+    // 璁╂帶浠剁獥鍙e昂瀵歌嚜鍔ㄥ尮閰嶅綋鍓嶅垪瀹�/琛屾暟锛堜笉鍑虹幇婊氬姩鏉★級
+    if (::IsWindow(m_grid.m_hWnd)) {
+        CSize best = m_grid.CalcBestWindowSize(TRUE, -1, 2, 2);
+        pItem->MoveWindow(rcItem.left, rcItem.top, best.cx, best.cy);
+        pItem->Invalidate();
+        pItem->GetWindowRect(&rcItem);
+        ScreenToClient(rcItem);
+        y = rcItem.bottom;
+        y += 18;
+    }
+
+    pItem = GetDlgItem(IDC_BUTTON_PORT1_PROCESS_START);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
+    x += 220;
+
+    pItem = GetDlgItem(IDC_BUTTON_PORT2_PROCESS_START);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
+    x += 220;
+
+    pItem = GetDlgItem(IDC_BUTTON_PORT3_PROCESS_START);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
+    x += 220;
+
+    pItem = GetDlgItem(IDC_BUTTON_PORT4_PROCESS_START);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
+    x += 220;
+}
+
+int CCjPage2::OnApply() 
+{
+    //SERVO::CProcessJob*
+    if (m_pContext == nullptr) return -1;
+    PJWarp* pPjWarp = (PJWarp*)m_pContext;
+    SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
+
+    // 鏇存柊鍚嶇О
+    BOOL bOkName = TRUE;
+    char szBuffer[256];
+    GetDlgItemText(IDC_EDIT_PJ_ID, szBuffer, 256);
+    for (auto item : m_pjWarps) {
+        if (item.pj != pProcessJob) {
+            SERVO::CProcessJob* temp = (SERVO::CProcessJob*)item.pj;
+            if (temp->id().compare(std::string(szBuffer)) == 0) {
+                bOkName = FALSE;
+                break;
+            }
+        }
+    }
+    if (!bOkName) {
+        AfxMessageBox("涓嶈兘浣跨敤鍜屽叾瀹働rocess Job鐩稿悓鐨処D");
+        return -1;
+    }
+
+
+    pProcessJob->setId(std::string(szBuffer));
+
+    // 鏇存柊閰嶆柟
+    CString strRecipe;
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
+    int idx = pComboBox->GetCurSel();
+    if (idx >= 0) {
+        pComboBox->GetLBText(idx, strRecipe);
+#ifdef UNICODE
+        CT2A utf8Str(strRecipe, CP_UTF8);
+        std::string recipe(utf8Str);
+#else
+        std::string recipe(strRecipe.GetString());
+#endif
+
+        pProcessJob->setRecipe(SERVO::RecipeMethod::NoTuning, recipe);
+    }
+
+
+    // 鏇存柊Port
+    int port = -1;
+    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4 };
+    for (int i = 0; i < 4; i++) {
+        int state = ((CButton*)GetDlgItem(ids[i]))->GetCheck();
+        if (state == BST_CHECKED) port = i;
+    }
+    pPjWarp->port = port;
+
+    if (pPjWarp->port != -1) {
+        for (int i = 0; i < 8; i++) {
+            pPjWarp->checkSlot[i] = m_grid.GetSlotChecked(pPjWarp->port, i);
+            pPjWarp->material[i] = m_grid.GetSlotMaterialType(pPjWarp->port, i);
+        }
+    }
+
+
+    ContentChanged(1);
+    return 0;
+}
+
+void CCjPage2::UpdatePjData()
+{
+    if (m_pContext == nullptr) return;
+
+    m_bContentChangedLock = TRUE;
+
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
+    pComboBox->ResetContent();
+    std::vector<std::string> vecRecipe = RecipeManager::getInstance().getAllPPID();
+    for (const auto& recipe : vecRecipe) {
+        pComboBox->AddString(CString(recipe.c_str()));
+    }
+
+
+    // ComboBox
+    PJWarp* pPjWarp = (PJWarp*)m_pContext;
+    SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
+    SetDlgItemText(IDC_EDIT_PJ_ID, pProcessJob->id().c_str());
+    int idx = pComboBox->FindStringExact(-1, pProcessJob->recipeSpec().c_str());
+    if (idx != CB_ERR) pComboBox->SetCurSel(idx);
+
+
+    // 4涓猚heckbox
+    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4};
+    static char* pszUsed[] = { "Port1(宸插崰鐢�)", "Port2(宸插崰鐢�)", "Port3(宸插崰鐢�)", "Port4(宸插崰鐢�)" };
+    static char* pszUnUsed[] = { "Port1(鍙敤)", "Port2(鍙敤)", "Port3(鍙敤)", "Port4(鍙敤)" };
+
+    int portIndex = -1;
+    bool enable[] = {true, true, true, true};
+    bool checked[] = { false, false, false, false };
+    for (auto item : m_pjWarps) {
+        if (0 <= item.port && item.port <= 4 && item.pj != ((PJWarp*)m_pContext)->pj) {
+            enable[item.port] = false;
+        }
+    }
+    if (0 <= ((PJWarp*)m_pContext)->port && ((PJWarp*)m_pContext)->port <= 3) {
+        checked[((PJWarp*)m_pContext)->port] = true;
+        portIndex = ((PJWarp*)m_pContext)->port;
+        m_nSelRadioId = ids[((PJWarp*)m_pContext)->port];
+    }
+
+    for (int i = 0; i < 4; i++) {
+        CButton* pButton = (CButton*)GetDlgItem(ids[i]);
+        pButton->SetCheck(checked[i] ? BST_CHECKED : BST_UNCHECKED);
+        pButton->SetWindowText(enable[i] ? pszUnUsed[i] : pszUsed[i]);
+        pButton->EnableWindow(enable[i]);
+
+        m_grid.SetPortAllocated(i, !checked[i], _T(""));
+        GetDlgItem(btnID[i])->EnableWindow(checked[i]);
+    }
+
+
+    // 璇诲彇鍑虹湡瀹炴暟鎹�
+    auto& master = theApp.m_model.getMaster();
+    int EQID[] = {EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4};
+    for (int p = 0; p < 4; p++) {
+        SERVO::CLoadPort* pPort = (SERVO::CLoadPort*)master.getEquipment(EQID[p]);
+        m_grid.SetPortInfo(p, 
+            (std::string("Port ") + std::to_string(p+1)).c_str(),
+            pPort->getCassetteId().c_str()
+        );
+        for (int i = 0; i < SLOT_MAX; ++i) {
+            SERVO::CSlot* pSlot = pPort->getSlot(i);
+            if (!pSlot) {
+                continue;
+            }
+
+            // 璁剧疆 Panel ID 
+            SERVO::CGlass* pGlass = dynamic_cast<SERVO::CGlass*>(pSlot->getContext());
+            SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
+            if (pGlass != nullptr && pJobDataS != nullptr) {
+                m_grid.SetSlotGlass(p, i, TRUE, 
+                    pGlass->getID().c_str(), 
+                    m_pjWarps[p].material[i]);
+            }
+            else {
+                m_grid.SetSlotGlass(p, i, FALSE, nullptr, CCarrierSlotGrid::MAT_G1);
+            }
+        }
+    }
+
+
+    // 璁剧疆鍕鹃�夋暟鎹�
+    if (portIndex != -1) {
+        for (int i = 0; i < 8; i++) {
+            m_grid.SetSlotChecked(portIndex, i, ((PJWarp*)m_pContext)->checkSlot[i]);
+        }
+    }
+
+    m_bContentChangedLock = FALSE;
+}
+
+void CCjPage2::OnEnChangeEditPjId()
+{
+    ContentChanged(0);
+}
+
+void CCjPage2::OnCbnSelchangeComboRecipe()
+{
+    ContentChanged(0);
+}
+
+void CCjPage2::OnBnClickedRadio1()
+{
+    BOOL lock[] = {TRUE, TRUE, TRUE, TRUE};
+    if (IDC_RADIO1 == m_nSelRadioId) {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
+        m_nSelRadioId = 0;
+    }
+    else {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO1);
+        m_nSelRadioId = IDC_RADIO1;
+        lock[0] = FALSE;
+    }
+
+    for (int i = 0; i < 4; i++) {
+        m_grid.SetPortAllocated(i, lock[i], _T(""));
+        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
+    }
+
+    ContentChanged(0);
+}
+
+void CCjPage2::OnBnClickedRadio2()
+{
+    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
+    if (IDC_RADIO2 == m_nSelRadioId) {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
+        m_nSelRadioId = 0;
+    }
+    else {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO2);
+        m_nSelRadioId = IDC_RADIO2;
+        lock[1] = FALSE;
+    }
+
+    for (int i = 0; i < 4; i++) {
+        m_grid.SetPortAllocated(i, lock[i], _T(""));
+        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
+    }
+
+    ContentChanged(0);
+}
+
+void CCjPage2::OnBnClickedRadio3()
+{
+    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
+    if (IDC_RADIO3 == m_nSelRadioId) {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
+        m_nSelRadioId = 0;
+    }
+    else {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO3);
+        m_nSelRadioId = IDC_RADIO3;
+        lock[2] = FALSE;
+    }
+
+    for (int i = 0; i < 4; i++) {
+        m_grid.SetPortAllocated(i, lock[i], _T(""));
+        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
+    }
+
+    ContentChanged(0);
+}
+
+void CCjPage2::OnBnClickedRadio4()
+{
+    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
+    if (IDC_RADIO4 == m_nSelRadioId) {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
+        m_nSelRadioId = 0;
+    }
+    else {
+        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO4);
+        m_nSelRadioId = IDC_RADIO4;
+        lock[3] = FALSE;
+    }
+
+    for (int i = 0; i < 4; i++) {
+        m_grid.SetPortAllocated(i, lock[i], _T(""));
+        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
+    }
+
+    ContentChanged(0);
+}
+
+void CCjPage2::OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    auto* nm = reinterpret_cast<CSG_SEL_CHANGE*>(pNMHDR);
+    const int port = nm->port;
+    const int slot = nm->slot;
+    const BOOL chk = nm->checked;
+
+    // 杩欓噷鍐欎綘鐨勪笟鍔¢�昏緫
+    // 渚嬪锛氭洿鏂扮姸鎬佹爮 / 鍚屾鍏跺畠鎺т欢 / 缁熻鏁伴噺
+    ContentChanged(0);
+
+    /*
+    if (m_pContext != nullptr) {
+        PJWarp* pjWarp = (PJWarp*)m_pContext;
+        for (int i = 0; i < 8; i++) {
+            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
+            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
+        }
+    }
+    */
+
+    *pResult = 0;
+}
+
+void CCjPage2::OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    auto* nm = reinterpret_cast<CSG_MAT_CHANGE*>(pNMHDR);
+    const int port = nm->port;
+    const int slot = nm->slot;
+    const int mat = nm->material; // 1/2
+
+    // 渚嬪锛氬嵆鍒诲埛鏂板彸渚ч瑙�/璁板綍鏃ュ織绛�
+    ContentChanged(0);
+
+    /*
+    if (m_pContext != nullptr) {
+        PJWarp* pjWarp = (PJWarp*)m_pContext;
+        for (int i = 0; i < 8; i++) {
+            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
+            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
+        }
+    }
+    */
+
+    *pResult = 0;
+}
+
+void CCjPage2::OnBnClickedButtonPort1ProcessStart()
+{
+    auto& master = theApp.m_model.getMaster();
+    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT1);
+    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
+
+}
+
+void CCjPage2::OnBnClickedButtonPort2ProcessStart()
+{
+    auto& master = theApp.m_model.getMaster();
+    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT2);
+    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
+}
+
+void CCjPage2::OnBnClickedButtonPort3ProcessStart()
+{
+    auto& master = theApp.m_model.getMaster();
+    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT3);
+    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
+}
+
+void CCjPage2::OnBnClickedButtonPort4ProcessStart()
+{
+    auto& master = theApp.m_model.getMaster();
+    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT4);
+    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
+}
+
+
diff --git a/SourceCode/Bond/Servo/CCjPage2.h b/SourceCode/Bond/Servo/CCjPage2.h
new file mode 100644
index 0000000..e9561cd
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage2.h
@@ -0,0 +1,58 @@
+锘�#pragma once
+#include "CCjPageBase.h"
+#include "ProcessJob.h"
+#include "CCarrierSlotGrid.h"
+
+
+
+// CPjPage1 瀵硅瘽妗�
+
+class CCjPage2 : public CCjPageBase
+{
+	DECLARE_DYNAMIC(CCjPage2)
+
+public:
+	CCjPage2(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CCjPage2();
+
+public:
+	void SetPjWarps(std::vector<PJWarp>& pjs);
+
+protected:
+	void Resize();
+	virtual int OnApply();
+	virtual void OnSetContext(void* pContext);
+
+private:
+	void UpdatePjData();
+
+private:
+	CCarrierSlotGrid m_grid;
+	std::vector<PJWarp> m_pjWarps;
+	int m_nSelRadioId;
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_CJ_PAGE1 };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnDestroy();
+	afx_msg void OnEnChangeEditPjId();
+	afx_msg void OnCbnSelchangeComboRecipe();
+	afx_msg void OnBnClickedRadio1();
+	afx_msg void OnBnClickedRadio2();
+	afx_msg void OnBnClickedRadio3();
+	afx_msg void OnBnClickedRadio4();
+	afx_msg void OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg void OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg void OnBnClickedButtonPort1ProcessStart();
+	afx_msg void OnBnClickedButtonPort2ProcessStart();
+	afx_msg void OnBnClickedButtonPort3ProcessStart();
+	afx_msg void OnBnClickedButtonPort4ProcessStart();
+};
diff --git a/SourceCode/Bond/Servo/CCjPage3.cpp b/SourceCode/Bond/Servo/CCjPage3.cpp
new file mode 100644
index 0000000..aac7455
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage3.cpp
@@ -0,0 +1,109 @@
+锘�// CPjPage1.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CCjPage3.h"
+#include "afxdialogex.h"
+
+
+// CPjPage1 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CCjPage3, CCjPageBase)
+
+CCjPage3::CCjPage3(CWnd* pParent /*=nullptr*/)
+	: CCjPageBase(IDD_CJ_PAGE3, pParent)
+{
+
+}
+
+CCjPage3::~CCjPage3()
+{
+}
+
+void CCjPage3::DoDataExchange(CDataExchange* pDX)
+{
+    CCjPageBase::DoDataExchange(pDX);
+}
+
+
+BEGIN_MESSAGE_MAP(CCjPage3, CCjPageBase)
+	ON_WM_DESTROY()
+    ON_EN_CHANGE(IDC_EDIT_CJ_ID, &CCjPage3::OnEnChangeEditCjId)
+END_MESSAGE_MAP()
+
+
+// CPjPage1 娑堟伅澶勭悊绋嬪簭
+
+
+BOOL CCjPage3::OnInitDialog()
+{
+    CCjPageBase::OnInitDialog();
+
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CCjPage3::OnDestroy()
+{
+    CCjPageBase::OnDestroy();
+
+	// TODO: 鍦ㄦ澶勬坊鍔犳秷鎭鐞嗙▼搴忎唬鐮�
+}
+
+void CCjPage3::Resize()
+{
+    CCjPageBase::Resize();
+
+    /*
+    CWnd* pItem;
+    CRect rcClient, rcItem;
+    GetClientRect(&rcClient);
+    pItem = GetDlgItem(IDC_LABEL_NO_SEL);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow((rcClient.Width() - rcItem.Width()) / 2,
+        (rcClient.Height() - rcItem.Height()) / 2,
+        rcItem.Width(), rcItem.Height());
+        */
+}
+
+int CCjPage3::OnApply()
+{
+    // SERVO::CControlJob*
+    if (m_pContext == nullptr) return -1;
+    SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_pContext;
+
+    // 鏇存柊鍚嶇О
+    BOOL bOkName = TRUE;
+    char szBuffer[256];
+    GetDlgItemText(IDC_EDIT_CJ_ID, szBuffer, 256);
+    pControlJob->setId(std::string(szBuffer));
+
+
+    ContentChanged(1);
+    return 0;
+}
+
+void CCjPage3::OnSetContext(void* pContext)
+{
+    UpdateCjData();
+}
+
+void CCjPage3::UpdateCjData()
+{
+    if (m_pContext == nullptr) return;
+
+    m_bContentChangedLock = TRUE;
+
+    SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_pContext;
+    SetDlgItemText(IDC_EDIT_CJ_ID, pControlJob->id().c_str());
+
+
+    m_bContentChangedLock = FALSE;
+}
+
+void CCjPage3::OnEnChangeEditCjId()
+{
+    ContentChanged(0);
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/CCjPage3.h b/SourceCode/Bond/Servo/CCjPage3.h
new file mode 100644
index 0000000..95764b3
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPage3.h
@@ -0,0 +1,35 @@
+锘�#pragma once
+#include "CCjPageBase.h"
+#include "CControlJob.h"
+
+
+// CPjPage1 瀵硅瘽妗�
+
+class CCjPage3 : public CCjPageBase
+{
+	DECLARE_DYNAMIC(CCjPage3)
+
+public:
+	CCjPage3(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CCjPage3();
+
+protected:
+	void Resize();
+	virtual int OnApply();
+	virtual void OnSetContext(void* pContext);
+	void UpdateCjData();
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_CJ_PAGE1 };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnDestroy();
+	afx_msg void OnEnChangeEditCjId();
+};
diff --git a/SourceCode/Bond/Servo/CCjPageBase.cpp b/SourceCode/Bond/Servo/CCjPageBase.cpp
new file mode 100644
index 0000000..d7c0391
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPageBase.cpp
@@ -0,0 +1,137 @@
+锘�// CPjPage1.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CCjPageBase.h"
+#include "afxdialogex.h"
+
+
+// CPjPage1 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CCjPageBase, CDialogEx)
+
+CCjPageBase::CCjPageBase(UINT nID, CWnd* pPage) : CDialogEx(nID, pPage)
+{
+    m_crBkgnd = RGB(255, 255, 255);
+    m_crBkgndCached = CLR_INVALID;
+    m_onContentChanged = nullptr;
+    m_bContentChangedLock = FALSE;
+    m_pContext = nullptr;
+    m_nContextType = 0;
+}
+
+CCjPageBase::~CCjPageBase()
+{
+}
+
+void CCjPageBase::DoDataExchange(CDataExchange* pDX)
+{
+	CDialogEx::DoDataExchange(pDX);
+}
+
+
+BEGIN_MESSAGE_MAP(CCjPageBase, CDialogEx)
+	ON_WM_CTLCOLOR()
+	ON_WM_SIZE()
+END_MESSAGE_MAP()
+
+
+// CPjPage1 娑堟伅澶勭悊绋嬪簭
+
+void CCjPageBase::SetTitle(CString strTitle)
+{
+    SetDlgItemText(IDC_LABEL_TITLE, strTitle);
+}
+
+void CCjPageBase::SetContext(void* pContext, int type)
+{
+    m_pContext = pContext;
+    m_nContextType = type;
+    OnSetContext(pContext);
+}
+
+void* CCjPageBase::GetContext()
+{
+    return m_pContext;
+}
+
+void CCjPageBase::SetOnContentChanged(ONCONTENTCHANGED onContentChanged)
+{
+    m_onContentChanged = onContentChanged;
+}
+
+BOOL CCjPageBase::OnInitDialog()
+{
+	CDialogEx::OnInitDialog();
+    Resize();
+
+
+    m_labelTitle.SubclassDlgItem(IDC_LABEL_TITLE, this);
+    m_labelTitle.Setpadding(PADDING_LEFT, 0);
+    m_labelTitle.Setpadding(PADDING_RIGHT, 0);
+
+ ;
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+
+HBRUSH CCjPageBase::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
+{
+    HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
+
+    // 鎯崇粰鍝簺鎺т欢鏀瑰簳鑹插氨鎶婂畠浠殑绫诲瀷鍐欒繘鏉ワ細
+    const bool needCustomBg =
+        (nCtlColor == CTLCOLOR_STATIC) ||
+        (nCtlColor == CTLCOLOR_DLG) ||   // 瀵硅瘽妗嗗簳鑹�
+        (nCtlColor == CTLCOLOR_BTN);        // 鎸夐挳锛堝彲閫夛級
+
+    if (needCustomBg)
+    {
+        // 鑻ョ涓�娆″垱寤猴紝鎴栭鑹叉敼鍙樺垯閲嶅缓
+        if (m_brBkgnd.GetSafeHandle() == nullptr || m_crBkgndCached != m_crBkgnd)
+        {
+            if (m_brBkgnd.GetSafeHandle())
+                m_brBkgnd.DeleteObject();
+
+            m_brBkgnd.CreateSolidBrush(m_crBkgnd);
+            m_crBkgndCached = m_crBkgnd;
+        }
+
+        // 鏂囨湰鍓嶆櫙/鑳屾櫙璁剧疆锛堜粎褰卞搷鏂囨湰缁樺埗锛�
+        pDC->SetBkColor(m_crBkgnd);
+        pDC->SetTextColor(RGB(0, 0, 0));
+        // 濡傞渶璁╅潤鎬佹枃鏈�忔槑鍙犲湪搴曡壊涓婏紝鍙敤锛�
+        // pDC->SetBkMode(TRANSPARENT);
+
+        return (HBRUSH)m_brBkgnd; // 瀹夊叏鐨勯殣寮忚浆鎹�
+    }
+
+    // 鍏朵粬鎺т欢绫诲瀷娌跨敤鍩虹被榛樿鐨勫埛瀛�
+    return hbr;
+}
+
+void CCjPageBase::OnSize(UINT nType, int cx, int cy)
+{
+	CDialogEx::OnSize(nType, cx, cy);
+    if (GetDlgItem(IDC_LABEL_TITLE) == nullptr) return;
+    Resize();
+}
+
+void CCjPageBase::Resize()
+{
+    CWnd* pItem;
+    CRect rcClient, rcItem;
+    GetClientRect(&rcClient);
+    pItem = GetDlgItem(IDC_LABEL_TITLE);
+    pItem->GetWindowRect(&rcItem);
+    pItem->MoveWindow(12, 8, rcClient.Width() - 24, rcItem.Height());
+}
+
+void CCjPageBase::ContentChanged(int code)
+{
+    if (!m_bContentChangedLock && m_onContentChanged != nullptr) {
+        m_onContentChanged(this, code, m_pContext, m_nContextType);
+    }
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/CCjPageBase.h b/SourceCode/Bond/Servo/CCjPageBase.h
new file mode 100644
index 0000000..03688f7
--- /dev/null
+++ b/SourceCode/Bond/Servo/CCjPageBase.h
@@ -0,0 +1,56 @@
+锘�#pragma once
+#include "GroupLabel.h"
+#include <functional>
+
+
+typedef std::function<void(void* pFrom, int code, void* pContext, int contextType)> ONCONTENTCHANGED;
+
+
+// CPjPage1 瀵硅瘽妗�
+
+class CCjPageBase : public CDialogEx
+{
+	DECLARE_DYNAMIC(CCjPageBase)
+
+public:
+	CCjPageBase(UINT nID, CWnd* pPage);			// 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CCjPageBase();
+
+public:
+	void SetTitle(CString strTitle);
+	virtual int OnApply() { return 0; };
+	void SetOnContentChanged(ONCONTENTCHANGED onContentChanged);
+	void SetContext(void* pContext, int type);
+	void* GetContext();
+
+protected:
+	virtual void Resize();
+	virtual void ContentChanged(int code);
+	virtual void OnSetContext(void* pContext) { };
+
+private:
+	COLORREF m_crBkgndCached;
+	CBrush m_brBkgnd;
+	CGroupLabel m_labelTitle;
+	ONCONTENTCHANGED m_onContentChanged;
+
+protected:
+	COLORREF m_crBkgnd;
+	BOOL m_bContentChangedLock;
+	void* m_pContext;
+	int m_nContextType;
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_CJ_PAGE1 };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
+	afx_msg void OnSize(UINT nType, int cx, int cy);
+};
diff --git a/SourceCode/Bond/Servo/CControlJob.cpp b/SourceCode/Bond/Servo/CControlJob.cpp
index 8dc2b18..cb1e794 100644
--- a/SourceCode/Bond/Servo/CControlJob.cpp
+++ b/SourceCode/Bond/Servo/CControlJob.cpp
@@ -4,9 +4,12 @@
 #include "SerializeUtil.h"
 
 static inline std::string trimCopy(std::string s) {
-    auto notspace = [](int ch) { return !std::isspace(ch); };
-    s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
-    s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+    s.erase(s.begin(),
+        std::find_if(s.begin(), s.end(),
+            [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }));
+    s.erase(std::find_if(s.rbegin(), s.rend(),
+        [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }).base(),
+        s.end());
     return s;
 }
 
@@ -35,6 +38,12 @@
         m_tEnd = src.m_tEnd;
     }
 
+    void CControlJob::setId(std::string& id)
+    {
+        m_cjId = trimCopy(id);
+        clampString(m_cjId, MAX_ID_LEN);
+    }
+
     bool CControlJob::addPJ(const std::string& pjId) {
         if (pjId.empty()) return false;
         auto id = pjId;
@@ -57,6 +66,28 @@
         return true;
     }
 
+    bool CControlJob::addPjPointer(CProcessJob* pj)
+    {
+        for (auto item : m_pjs) {
+            if (item->id().compare(pj->id()) == 0) return false;
+        }
+
+        m_pjs.push_back(pj);
+        return true;
+    }
+
+    bool CControlJob::removePjPointer(const std::string& id)
+    {
+        for(auto iter = m_pjs.begin(); iter != m_pjs.end(); ++iter) {
+            if ((*iter)->id().compare(id) == 0) {
+                m_pjs.erase(iter);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     bool CControlJob::removePJ(const std::string& pjId) {
         auto it = std::find(m_pjIds.begin(), m_pjIds.end(), pjId);
         if (it == m_pjIds.end()) return false;
@@ -71,6 +102,11 @@
     const std::vector<CControlJob::ValidationIssue>& CControlJob::issues()
     {
         return m_issues;
+    }
+
+    void CControlJob::clearIssues()
+    {
+        m_issues.clear();
     }
 
     bool CControlJob::validateForCreate(
@@ -184,9 +220,11 @@
         return true;
     }
 
-    bool CControlJob::abort() {
+    bool CControlJob::abort(std::string reason) {
         if (m_state == CJState::Completed || m_state == CJState::Aborted || m_state == CJState::Failed)
             return false;
+        m_failReason = trimCopy(reason);
+        clampString(m_failReason, 128);
         m_state = CJState::Aborted;
         markEnd();
         return true;
@@ -281,23 +319,12 @@
         out = CControlJob(cjId);
         out.setPriority(prio);
 
-        // 直接恢复内部状态(若你要求走状态机,可在这里按合法过渡调用 queue()/start()/...)
-        // 简化:直接赋值(你在 CControlJob.cpp 内部,可访问私有成员)
-        struct Access : CControlJob {
-            using CControlJob::m_state;
-            using CControlJob::m_failReason;
-            using CControlJob::m_tQueued;
-            using CControlJob::m_tStart;
-            using CControlJob::m_tEnd;
-            using CControlJob::m_pjIds;
-        };
-        auto& a = reinterpret_cast<Access&>(out);
-        a.m_state = static_cast<CJState>(st);
-        a.m_failReason = std::move(failText);
-        a.m_tQueued = std::move(tQ);
-        a.m_tStart = std::move(tS);
-        a.m_tEnd = std::move(tE);
-        a.m_pjIds = std::move(pjIds);
+        out.m_state = static_cast<CJState>(st);
+        out.m_failReason = std::move(failText);
+        out.m_tQueued = std::move(tQ);
+        out.m_tStart = std::move(tS);
+        out.m_tEnd = std::move(tE);
+        out.m_pjIds = std::move(pjIds);
 
         return true;
     }
diff --git a/SourceCode/Bond/Servo/CControlJob.h b/SourceCode/Bond/Servo/CControlJob.h
index 8533018..1c391e1 100644
--- a/SourceCode/Bond/Servo/CControlJob.h
+++ b/SourceCode/Bond/Servo/CControlJob.h
@@ -42,6 +42,7 @@
 
         // —— 基本属性 —— //
         const std::string& id()     const noexcept { return m_cjId; }
+        void setId(std::string& id);
         CJState            state()  const noexcept { return m_state; }
         uint8_t            priority() const noexcept { return m_priority; }
         void               setPriority(uint8_t p) noexcept { m_priority = p; }
@@ -54,6 +55,8 @@
         bool containsPJ(const std::string& pjId) const;
         const std::vector<std::string>& pjIds() const noexcept { return m_pjIds; }
         bool setPJs(const std::vector<CProcessJob*>& pjs);
+        bool removePjPointer(const std::string& id);
+        bool addPjPointer(CProcessJob* pj);
         void clearPJs() { m_pjIds.clear(); }
         const std::vector<CProcessJob*>& getPjs() { return m_pjs; };
 
@@ -68,7 +71,8 @@
             const std::function<bool(const std::string&)>& getPjExistsFn,
             const std::function<bool(const std::string&)>& canJoinFn
         );
-        const std::vector<CControlJob::ValidationIssue>& CControlJob::issues();
+        const std::vector<ValidationIssue>& CControlJob::issues();
+        void clearIssues();
 
         // —— S14F9 → S14F10 的“应用结果”模型 —— //
         struct CreateRequest {
@@ -95,7 +99,7 @@
         bool pause();          // Executing -> Paused
         bool resume();         // Paused -> Executing
         bool complete();       // Executing/Paused -> Completed
-        bool abort();          // 非终态 -> Aborted
+        bool abort(std::string reason);          // 非终态 -> Aborted
         bool fail(std::string reason); // 任意 -> Failed
 
         const std::string& failReason() const noexcept { return m_failReason; }
diff --git a/SourceCode/Bond/Servo/CControlJobDlg.cpp b/SourceCode/Bond/Servo/CControlJobDlg.cpp
index 7365c3d..2a57dcd 100644
--- a/SourceCode/Bond/Servo/CControlJobDlg.cpp
+++ b/SourceCode/Bond/Servo/CControlJobDlg.cpp
@@ -6,13 +6,35 @@
 #include "CControlJobDlg.h"
 #include "afxdialogex.h"
 
+// ===== 鏂板锛氭爣鍑嗗簱澶� =====
+#include <array>
+#include <string>
+#include <unordered_set>
+#include <algorithm>
+
+// ===== 鏂板锛欳String 鐨� Hash/Equal锛堣法 ANSI/Unicode 閫氬悆锛�=====
+struct CStringHash {
+    size_t operator()(const CString& s) const noexcept {
+#ifdef _UNICODE
+        return std::hash<std::wstring>{}(std::wstring(s.GetString()));
+#else
+        return std::hash<std::string>{}(std::string(s.GetString()));
+#endif
+    }
+};
+struct CStringEqual {
+    bool operator()(const CString& a, const CString& b) const noexcept {
+        return a == b;
+    }
+};
+
 
 // CControlJobDlg 瀵硅瘽妗�
 
 IMPLEMENT_DYNAMIC(CControlJobDlg, CDialogEx)
 
 CControlJobDlg::CControlJobDlg(CWnd* pParent /*=nullptr*/)
-	: CDialogEx(IDD_DIALOG_CONTROL_JOB, pParent)
+    : CDialogEx(IDD_DIALOG_CONTROL_JOB, pParent)
 {
     m_pControlJob = nullptr;
 }
@@ -23,13 +45,16 @@
 
 void CControlJobDlg::DoDataExchange(CDataExchange* pDX)
 {
-	CDialogEx::DoDataExchange(pDX);
-	DDX_Control(pDX, IDC_LIST1, m_listCtrl);
+    CDialogEx::DoDataExchange(pDX);
+    DDX_Control(pDX, IDC_LIST1, m_listCtrl);
 }
 
 
 BEGIN_MESSAGE_MAP(CControlJobDlg, CDialogEx)
     ON_WM_SIZE()
+    ON_BN_CLICKED(IDC_BUTTON_COMPLETION_BATH, &CControlJobDlg::OnBnClickedButtonCompletionBath)
+    ON_BN_CLICKED(IDC_BUTTON_RELOAD, &CControlJobDlg::OnBnClickedButtonReload)
+    ON_WM_TIMER()
 END_MESSAGE_MAP()
 
 
@@ -41,7 +66,7 @@
 // CControlJobDlg 娑堟伅澶勭悊绋嬪簭
 BOOL CControlJobDlg::OnInitDialog()
 {
-	CDialogEx::OnInitDialog();
+    CDialogEx::OnInitDialog();
 
 
     // label瀛椾綋
@@ -68,12 +93,12 @@
 
     // 鎺т欢鐘舵��
     Resize();
-    ShowGroup1(m_pControlJob == nullptr);
-    ShowGroup2(m_pControlJob != nullptr);
-    LoadData();
+    OnBnClickedButtonReload();
+    SetTimer(1, 2000, nullptr);
 
-	return TRUE;  // return TRUE unless you set the focus to a control
-				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+
+    return TRUE;  // return TRUE unless you set the focus to a control
+                  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
 }
 
 void CControlJobDlg::OnSize(UINT nType, int cx, int cy)
@@ -92,15 +117,30 @@
 
 
     // 鍏抽棴鎸夐挳
+    int x2 = rcClient.right - 12;
     int y = rcClient.bottom - 12;
     pItem = GetDlgItem(IDCANCEL);
     pItem->GetClientRect(&rcItem);
-    pItem->MoveWindow(rcClient.right - 12 - rcItem.Width(),
+    pItem->MoveWindow(x2 - rcItem.Width(),
         y - rcItem.Height(),
+        rcItem.Width(), rcItem.Height());
+    x2 -= rcItem.Width();
+    x2 -= 8;
+
+    pItem = GetDlgItem(IDC_BUTTON_RELOAD);
+    pItem->GetClientRect(&rcItem);
+    pItem->MoveWindow(x2 - rcItem.Width(),
+        y - rcItem.Height(),
+        rcItem.Width(), rcItem.Height());
+
+
+    // 缁撴壒鎸夐挳
+    pItem = GetDlgItem(IDC_BUTTON_COMPLETION_BATH);
+    pItem->GetClientRect(&rcItem);
+    pItem->MoveWindow(12, y - rcItem.Height(),
         rcItem.Width(), rcItem.Height());
     y -= rcItem.Height();
     y -= 12;
-
 
     // 绾�
     pItem = GetDlgItem(IDC_LINE1);
@@ -132,38 +172,239 @@
     GetDlgItem(IDC_LIST1)->ShowWindow(bShow ? SW_SHOW : SW_HIDE);
 }
 
-void CControlJobDlg::LoadData()
+void CControlJobDlg::LoadData(SERVO::CControlJob* pControlJob)
 {
-    m_listCtrl.DeleteAllItems();
+    // 鈥斺�� 宸ュ叿锛氭寜鈥滅涓�鍒楅敭鈥濆湪 parent 涓嬫煡鎵惧瓙鑺傜偣
+    auto FindChildByKey = [&](CExpandableListCtrl::Node* parent, LPCTSTR key)->CExpandableListCtrl::Node* {
+        if (!parent) return nullptr;
+        for (auto& up : parent->children) {
+            auto* n = up.get();
+            if (n && n->cols.size() > 0 && n->cols[0].CompareNoCase(key) == 0)
+                return n;
+        }
+        return nullptr;
+    };
 
-    if (m_pControlJob != nullptr) {
-        auto* root1 = m_listCtrl.InsertRoot({ m_pControlJob->id().c_str(), _T("ControlJob"), 
-            m_pControlJob->getStateText().c_str(), _T("") });
-        auto pjs = m_pControlJob->getPjs();
+    // 鈥斺�� 宸ュ叿锛氭湁鍒欐洿鏂般�佹棤鍒欏垱寤猴紙6鍒楋級
+    auto EnsureChild = [&](CExpandableListCtrl::Node* parent, const std::array<CString, 6>& cols)->CExpandableListCtrl::Node* {
+        CExpandableListCtrl::Node* n = FindChildByKey(parent, cols[0]);
+        if (!n) {
+            n = m_listCtrl.InsertChild(parent, { cols[0], cols[1], cols[2], cols[3], cols[4], cols[5] });
+        }
+        else {
+            if ((int)n->cols.size() < 6) n->cols.resize(6);
+            for (int i = 0; i < 6; i++) if (n->cols[i] != cols[i]) n->cols[i] = cols[i];
+        }
+        return n;
+    };
+
+    // 鈥斺�� 宸ュ叿锛氬垹闄� parent 涓嬧�滄湰娆℃湭鍑虹幇鈥濈殑瀛愯妭鐐癸紙鍩轰簬绗竴鍒楅敭锛�
+    auto RemoveStaleChildren = [&](CExpandableListCtrl::Node* parent, const std::unordered_set<CString, CStringHash, CStringEqual>& keep) {
+        if (!parent) return;
+        auto& vec = parent->children;
+        vec.erase(std::remove_if(vec.begin(), vec.end(), [&](const std::unique_ptr<CExpandableListCtrl::Node>& up) {
+            auto* n = up.get();
+            if (!n || n->cols.empty()) return true; // 闃插尽锛氭棤鏁堣鐩存帴鍒�
+            return keep.find(n->cols[0]) == keep.end();
+            }), vec.end());
+    };
+
+    // 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�� 1) 鏃犳暟鎹細娓呯┖骞跺浣嶇紦瀛� 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺��
+    if (!pControlJob) {
+        m_listCtrl.ClearTree();
+        m_rootNode = nullptr;
+        m_lastCjPtr = nullptr;
+        m_lastCjId.Empty();
+        m_listCtrl.RebuildVisible();
+        return;
+    }
+
+    const CString curId = pControlJob->id().c_str();
+    const bool cjChanged = (pControlJob != m_lastCjPtr) || (curId != m_lastCjId);
+
+    // 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�� 2) CJ 鍙樹簡锛氭暣妫甸噸寤猴紙淇濈暀灞曞紑鏍囪涓嶅繀绠★紝閲嶅缓鍗冲彲锛� 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺��
+    if (cjChanged || !m_rootNode) {
+        m_listCtrl.ClearTree();
+
+        m_rootNode = m_listCtrl.InsertRoot({
+            pControlJob->id().c_str(), _T("ControlJob"),
+            pControlJob->getStateText().c_str(), _T(""), _T(""),
+            pControlJob->failReason().c_str()
+            });
+
+        auto pjs = pControlJob->getPjs();
         for (auto pj : pjs) {
-            auto* root2 = m_listCtrl.InsertChild(root1, {pj->id().c_str(),  _T("ProcessJob"),
-                pj->getStateText().c_str(), pj->recipeSpec().c_str(), _T(""), _T(""), _T("") });
+            auto* pjNode = m_listCtrl.InsertChild(m_rootNode, {
+                pj->id().c_str(), _T("ProcessJob"),
+                pj->getStateText().c_str(), pj->recipeSpec().c_str(), _T(""),
+                pj->failReason().c_str()
+                });
+
             auto cs = pj->carriers();
             for (auto c : cs) {
                 for (auto g : c.contexts) {
-                    SERVO::CGlass* pGlass = (SERVO::CGlass*)g;
-                    if (pGlass != nullptr) {
-                        int port, slot;
+                    auto* pGlass = static_cast<SERVO::CGlass*>(g);
+                    if (pGlass) {
+                        int port = 0, slot = 0;
                         pGlass->getOrginPort(port, slot);
-                        std::string carrier = c.carrierId + " / Port" + std::to_string(port + 1) + " / Slot" + std::to_string(slot + 1);
-                        m_listCtrl.InsertChild(root2, { pGlass->getID().c_str(), _T("Glass"),
-                            pGlass->getStateText().c_str(), _T(""), carrier.c_str(), _T("") });
+                        CString carrier; carrier.Format(_T("%s / Port%d / Slot%d"),
+                            CString(c.carrierId.c_str()), port + 1, slot + 1);
+
+                        m_listCtrl.InsertChild(pjNode, {
+                            pGlass->getID().c_str(), _T("Glass"),
+                            pGlass->getStateText().c_str(), _T(""),
+                            carrier, _T("")
+                            });
                     }
                     else {
-                        m_listCtrl.InsertChild(root2, { "Null", _T("Glass"), _T(""), _T(""), c.carrierId.c_str(), _T("") });
+                        m_listCtrl.InsertChild(pjNode, {
+                            _T("Null@") + CString(c.carrierId.c_str()), _T("Glass"),
+                            _T(""), _T(""), CString(c.carrierId.c_str()), _T("")
+                            });
                     }
                 }
             }
-            root2->expanded = true;
+            pjNode->expanded = true;
         }
-        root1->expanded = true;
+        m_rootNode->expanded = true;
 
+        m_lastCjPtr = pControlJob;
+        m_lastCjId = curId;
 
         m_listCtrl.RebuildVisible();
+        return;
     }
+
+    // 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�� 3) CJ 鏈彉锛氬閲忔洿鏂� 鈥斺�斺�斺�斺�斺�斺�斺�斺�斺�斺�斺��
+
+    // 3.1 鏇存柊 CJ 琛屾枃鏈紙鐘舵�佸彲鑳藉彉鍖栵級
+    if ((int)m_rootNode->cols.size() < 6) m_rootNode->cols.resize(6);
+    m_rootNode->cols[0] = pControlJob->id().c_str();
+    m_rootNode->cols[1] = _T("ControlJob");
+    m_rootNode->cols[2] = pControlJob->getStateText().c_str();
+    // cols[3] 淇濈暀涓虹┖
+    // cols[4] 淇濈暀涓虹┖
+    m_rootNode->cols[5] = pControlJob->failReason().c_str();
+
+    // 3.2 鍚屾 PJ 灞�
+    std::unordered_set<CString, CStringHash, CStringEqual> pjKeysWanted;
+
+    auto pjs = pControlJob->getPjs();
+    for (auto pj : pjs) {
+        CString pjId = pj->id().c_str();
+        pjKeysWanted.insert(pjId);
+
+        auto* pjNode = FindChildByKey(m_rootNode, pjId);
+        if (!pjNode) {
+            pjNode = m_listCtrl.InsertChild(m_rootNode, {
+                pjId, _T("ProcessJob"),
+                pj->getStateText().c_str(), pj->recipeSpec().c_str(), _T(""),
+                pj->failReason().c_str()
+                });
+            pjNode->expanded = true; // 鍒濇鍑虹幇榛樿灞曞紑
+        }
+        else {
+            if ((int)pjNode->cols.size() < 6) pjNode->cols.resize(6);
+            pjNode->cols[1] = _T("ProcessJob");
+            pjNode->cols[2] = pj->getStateText().c_str();
+            pjNode->cols[3] = pj->recipeSpec().c_str();
+            pjNode->cols[5] = pj->failReason().c_str();
+        }
+
+        // 3.3 鍚屾 Glass 灞傦紙绗竴鍒楅敭锛欸lassID锛涚┖瀵硅薄鐢� "Null@CarrierId" 闃插啿绐侊級
+        std::unordered_set<CString, CStringHash, CStringEqual> gKeysWanted;
+
+        auto cs = pj->carriers();
+        for (auto c : cs) {
+            for (auto g : c.contexts) {
+                auto* pGlass = static_cast<SERVO::CGlass*>(g);
+                if (pGlass) {
+                    int port = 0, slot = 0;
+                    pGlass->getOrginPort(port, slot);
+                    CString carrier; carrier.Format(_T("%s / Port%d / Slot%d"),
+                        CString(c.carrierId.c_str()), port + 1, slot + 1);
+
+                    CString gid = pGlass->getID().c_str();
+                    gKeysWanted.insert(gid);
+
+                    auto* rowG = FindChildByKey(pjNode, gid);
+                    if (!rowG) {
+                        m_listCtrl.InsertChild(pjNode, {
+                            gid, _T("Glass"),
+                            pGlass->getStateText().c_str(), _T(""),
+                            carrier, _T("")
+                            });
+                    }
+                    else {
+                        if ((int)rowG->cols.size() < 6) rowG->cols.resize(6);
+                        rowG->cols[1] = _T("Glass");
+                        rowG->cols[2] = pGlass->getStateText().c_str();
+                        rowG->cols[3] = _T("");
+                        rowG->cols[4] = carrier;
+                        rowG->cols[5] = _T("");
+                    }
+                }
+                else {
+                    CString key = _T("Null@") + CString(c.carrierId.c_str());
+                    gKeysWanted.insert(key);
+
+                    auto* rowG = FindChildByKey(pjNode, key);
+                    if (!rowG) {
+                        m_listCtrl.InsertChild(pjNode, {
+                            key, _T("Glass"), _T(""), _T(""),
+                            CString(c.carrierId.c_str()), _T("")
+                            });
+                    }
+                    else {
+                        if ((int)rowG->cols.size() < 6) rowG->cols.resize(6);
+                        rowG->cols[1] = _T("Glass");
+                        rowG->cols[2] = _T("");
+                        rowG->cols[3] = _T("");
+                        rowG->cols[4] = CString(c.carrierId.c_str());
+                        rowG->cols[5] = _T("");
+                    }
+                }
+            }
+        }
+
+        // 鍒犻櫎鏈涓嶅瓨鍦ㄧ殑 Glass 瀛愯妭鐐�
+        RemoveStaleChildren(pjNode, gKeysWanted);
+    }
+
+    // 鍒犻櫎鏈涓嶅瓨鍦ㄧ殑 PJ 瀛愯妭鐐�
+    RemoveStaleChildren(m_rootNode, pjKeysWanted);
+
+    // 3.4 閲嶅缓鍙琛岋紙涓嶆敼鍙� expanded 鏍囧織锛岄伩鍏嶉棯鐑�/鎶樺彔鐘舵�佷涪澶憋級
+    m_listCtrl.RebuildVisible();
+}
+
+void CControlJobDlg::OnBnClickedButtonCompletionBath()
+{
+    if (theApp.m_model.getMaster().forceCompleteControlJob("娴嬭瘯鎵嬪姩缁撴壒")) {
+        AfxMessageBox("缁撴壒瀹屾垚");
+    }
+
+    OnBnClickedButtonReload();
+}
+
+void CControlJobDlg::OnBnClickedButtonReload()
+{
+    auto* cj = m_pControlJob;
+    if (cj == nullptr) {
+        cj = theApp.m_model.getMaster().getControlJob();
+    }
+
+    ShowGroup1(cj == nullptr);
+    ShowGroup2(cj != nullptr);
+    GetDlgItem(IDC_BUTTON_COMPLETION_BATH)->EnableWindow(cj != nullptr);
+    LoadData(cj);
+}
+
+void CControlJobDlg::OnTimer(UINT_PTR nIDEvent)
+{
+    if (1 == nIDEvent) {
+        OnBnClickedButtonReload();
+    }
+
+    CDialogEx::OnTimer(nIDEvent);
 }
diff --git a/SourceCode/Bond/Servo/CControlJobDlg.h b/SourceCode/Bond/Servo/CControlJobDlg.h
index 410f3ec..ed4bdff 100644
--- a/SourceCode/Bond/Servo/CControlJobDlg.h
+++ b/SourceCode/Bond/Servo/CControlJobDlg.h
@@ -20,11 +20,16 @@
 	void Resize();
 	void ShowGroup1(BOOL bShow);
 	void ShowGroup2(BOOL bShow);
-	void LoadData();
+	void LoadData(SERVO::CControlJob* pControlJob);
 
 private:
 	SERVO::CControlJob* m_pControlJob;
 	CFont m_fontNoJob;
+
+	// 璁板綍涓婁竴娆$殑 CJ 韬唤锛堟寚閽�/ID锛�
+	SERVO::CControlJob* m_lastCjPtr = nullptr;
+	CString             m_lastCjId;
+	CExpandableListCtrl::Node* m_rootNode = nullptr;
 
 protected:
 	CExpandableListCtrl m_listCtrl;
@@ -42,4 +47,7 @@
 public:
 	virtual BOOL OnInitDialog();
 	afx_msg void OnSize(UINT nType, int cx, int cy);
+	afx_msg void OnBnClickedButtonCompletionBath();
+	afx_msg void OnBnClickedButtonReload();
+	afx_msg void OnTimer(UINT_PTR nIDEvent);
 };
diff --git a/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp b/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
new file mode 100644
index 0000000..2e2d92c
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
@@ -0,0 +1,652 @@
+锘�// CControlJobManagerDlg.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CControlJobManagerDlg.h"
+#include "afxdialogex.h"
+#include "ToolUnits.h"
+#include "RecipeManager.h"
+
+
+bool CControlJobManagerDlg::m_bHasState = false;
+CControlJobManagerDlg::State CControlJobManagerDlg::m_state{};
+
+// CControlJobManagerDlg 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CControlJobManagerDlg, CDialogEx)
+
+CControlJobManagerDlg::CControlJobManagerDlg(CWnd* pParent /*=nullptr*/)
+	: CDialogEx(IDD_DIALOG_CONTROL_JOB_MANAGER, pParent)
+{
+	m_pControlJob = nullptr;
+}
+
+CControlJobManagerDlg::~CControlJobManagerDlg()
+{
+
+}
+
+void CControlJobManagerDlg::FreeState()
+{
+	if (!m_bHasState) return;
+	for (auto item : m_state.pjWarps) {
+		delete (SERVO::CProcessJob*)item.pj;
+	}
+	m_state.pjWarps.clear();
+
+	if (m_state.pControlJob != nullptr) {
+		delete m_state.pControlJob;
+		m_state.pControlJob = nullptr;
+	}
+	m_bHasState = false;
+}
+
+void CControlJobManagerDlg::DoDataExchange(CDataExchange* pDX)
+{
+	CDialogEx::DoDataExchange(pDX);
+	DDX_Control(pDX, IDC_TREE1, m_tree);	
+}
+
+
+BEGIN_MESSAGE_MAP(CControlJobManagerDlg, CDialogEx)
+	ON_WM_SIZE()
+	ON_WM_GETMINMAXINFO()
+	ON_NOTIFY(TVN_ITEMCHANGED, IDC_TREE1, &CControlJobManagerDlg::OnTvnItemChangedTree)
+	ON_NOTIFY(NM_CLICK, IDC_TREE1, &CControlJobManagerDlg::OnTreeClick)          // 鏂板
+	ON_NOTIFY(TVN_KEYDOWN, IDC_TREE1, &CControlJobManagerDlg::OnTreeKeyDown)        // 鏂板
+	ON_MESSAGE(WM_AFTER_TVCHECK, &CControlJobManagerDlg::OnAfterTvCheck)                // 鏂板
+	ON_WM_DESTROY()
+	ON_BN_CLICKED(IDC_BUTTON_APPLY, &CControlJobManagerDlg::OnBnClickedButtonApply)
+	ON_NOTIFY(TVN_SELCHANGING, IDC_TREE1, &CControlJobManagerDlg::OnTvnSelchangingTree1)
+	ON_BN_CLICKED(IDC_BUTTON_BATH_COMPLETION, &CControlJobManagerDlg::OnBnClickedButtonBathCompletion)
+END_MESSAGE_MAP()
+
+
+// CControlJobManagerDlg 娑堟伅澶勭悊绋嬪簭
+
+
+BOOL CControlJobManagerDlg::OnInitDialog()
+{
+	CDialogEx::OnInitDialog();
+
+	auto onContentChanged = [&](void* pFrom, int code, void* pContext, int contextType) -> void {
+		if (0 == code) {
+			GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(TRUE);
+		}
+		else if (1 == code) {
+			if (contextType == 1) {
+				UpProcessJobId((PJWarp*)pContext);
+			}
+			else if (contextType == 2) {
+				UpControlJobId((SERVO::CControlJob*)pContext);
+			}
+		}
+	};
+
+	// page1
+	CCjPage1* pPage1 = new CCjPage1(this);
+	pPage1->Create(IDD_CJ_PAGE1, this);
+	pPage1->SetTitle(_T("鏈�夋嫨"));
+	pPage1->SetOnContentChanged(onContentChanged);
+	pPage1->ShowWindow(SW_SHOW);
+	m_pages.push_back(pPage1);
+
+	// page2
+	CCjPage2* pPage2 = new CCjPage2(this);
+	pPage2->Create(IDD_CJ_PAGE2, this);
+	pPage2->SetTitle(_T("ProcessJob"));
+	pPage2->SetOnContentChanged(onContentChanged);
+	m_pages.push_back(pPage2);
+
+	// page3
+	CCjPage3* pPage3 = new CCjPage3(this);
+	pPage3->Create(IDD_CJ_PAGE3, this);
+	pPage3->SetTitle(_T("ControlJob"));
+	pPage3->SetOnContentChanged(onContentChanged);
+	m_pages.push_back(pPage3);
+
+
+	// tree
+	m_tree.ModifyStyle(0, TVS_CHECKBOXES);
+
+
+	InitData();
+	UpdateControlJob();
+	UpdateCtrlState();
+	Resize();
+
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CControlJobManagerDlg::OnSize(UINT nType, int cx, int cy)
+{
+	CDialogEx::OnSize(nType, cx, cy);
+	if (GetDlgItem(IDC_TREE1) == nullptr) return;
+
+	Resize();
+}
+
+void CControlJobManagerDlg::Resize()
+{
+	CWnd* pItem;
+	CRect rcClient, rcItem;
+	GetClientRect(&rcClient);
+
+	GetDlgItem(IDCANCEL)->GetWindowRect(&rcItem);
+	ScreenToClient(&rcItem);
+
+	const int LEFTWIDTH = 218;
+	int x = 12, y = 12;
+	int x2 = rcClient.right - 12;
+	int y2 = rcClient.bottom - 12;
+
+
+	// 鍏堢Щ鍔ㄦ寜閽�
+	pItem = GetDlgItem(IDC_BUTTON_APPLY);
+	pItem->GetWindowRect(&rcItem);
+	pItem->MoveWindow(x2 - rcItem.Width(), y2 - rcItem.Height(), rcItem.Width(), rcItem.Height());
+	x2 -= rcItem.Width();
+	x2 -= 8;
+
+	pItem = GetDlgItem(IDC_BUTTON_BATH_COMPLETION);
+	pItem->GetWindowRect(&rcItem);
+	pItem->MoveWindow(x2 - rcItem.Width(), y2 - rcItem.Height(), rcItem.Width(), rcItem.Height());
+	x += rcItem.Width();
+	x += 8;
+	
+	y2 -= rcItem.Height();
+	y2 -= 8;
+
+
+	// 鏍戞帶浠�
+	x = 12;
+	y = 12;
+	pItem = GetDlgItem(IDC_TREE1);
+	pItem->MoveWindow(x, y, LEFTWIDTH, y2 - y);
+	x += LEFTWIDTH;
+	x += 5;
+
+
+	// 瀛愰〉闈�
+	x2 = rcClient.right - 12;
+	for (auto page : m_pages) {
+		page->MoveWindow(x, 12, x2 - x, y2 - 12);
+	}
+
+}
+
+void CControlJobManagerDlg::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
+{
+	CDialogEx::OnGetMinMaxInfo(lpMMI);
+
+	// 璁剧疆鏈�灏忓楂橈紙姣斿 400x300锛�
+	lpMMI->ptMinTrackSize.x = 600;
+	lpMMI->ptMinTrackSize.y = 400;
+
+	// 涔熷彲浠ラ『渚胯缃渶澶у楂�
+	// lpMMI->ptMaxTrackSize.x = 800;
+	// lpMMI->ptMaxTrackSize.y = 600;
+}
+
+void CControlJobManagerDlg::UpdateCtrlState()
+{
+	GetDlgItem(IDC_BUTTON_BATH_COMPLETION)->EnableWindow(true);
+}
+
+void CControlJobManagerDlg::UpdateControlJob()
+{
+	m_tree.DeleteAllItems();
+	if (m_pControlJob == nullptr) return;
+
+	HTREEITEM hRoot = m_tree.InsertItem(m_pControlJob->id().c_str(), 0, 0);
+	m_tree.SetItemData(hRoot, (DWORD_PTR)m_pControlJob);
+	m_tree.SetItemState(hRoot, 0, TVIS_STATEIMAGEMASK);
+	for (auto& item : m_pjWarps) {
+		HTREEITEM hItem = m_tree.InsertItem(((SERVO::CProcessJob*)item.pj)->id().c_str(), 0, 0, hRoot);
+		m_tree.SetItemData(hItem, (DWORD_PTR)&item);
+		m_tree.SetItemState(hItem, INDEXTOSTATEIMAGEMASK(item.addToCj ? 2 : 1), TVIS_STATEIMAGEMASK);
+	}
+	m_tree.Expand(hRoot, TVE_EXPAND);
+}
+
+bool CControlJobManagerDlg::AddPorcessJob(SERVO::CProcessJob* pj)
+{
+	if (m_pControlJob == nullptr) return false;
+	return m_pControlJob->addPjPointer(pj);
+}
+
+bool CControlJobManagerDlg::RemovePorcessJob(SERVO::CProcessJob* pj)
+{
+	if (m_pControlJob == nullptr) return false;
+	return m_pControlJob->removePjPointer(pj->id());
+}
+
+void CControlJobManagerDlg::OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult)
+{
+	auto* p = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
+	UINT oldState = p->itemOld.state, newState = p->itemNew.state;
+	HTREEITEM hItem = p->itemNew.hItem;
+
+	if (((oldState ^ newState) & TVIS_STATEIMAGEMASK) != 0) {
+		const int idx = (newState & TVIS_STATEIMAGEMASK) >> 12; // 1=鏈��,2=宸查��
+		const bool checked = (idx == 2);
+
+		PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hItem);
+		if (pjWarp != nullptr) {
+			CString s; s.Format("%s %d", ((SERVO::CProcessJob*)pjWarp->pj)->id().c_str(), 
+				checked ? "" : "");
+			AfxMessageBox(s);
+		}
+	}
+
+
+	*pResult = 0;
+}
+
+// 鍛戒腑澶嶉�夋锛氱敤 NM_CLICK 鍋氬懡涓祴璇曪紝鐒跺悗鈥滄粸鍚庘�濊鍙栨柊鐘舵��
+void CControlJobManagerDlg::OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult)
+{
+	*pResult = 0;
+
+	DWORD pos = ::GetMessagePos();
+	CPoint pt(GET_X_LPARAM(pos), GET_Y_LPARAM(pos));
+	m_tree.ScreenToClient(&pt);
+
+	TVHITTESTINFO ht{}; ht.pt = pt;
+	HTREEITEM hItem = m_tree.HitTest(&ht);
+	if (hItem && (ht.flags & TVHT_ONITEMSTATEICON)) {
+		// 璁� TreeView 鍏堝垏鎹紝鍐嶅紓姝ヨ鍙栨渶缁堢姸鎬�
+		PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
+	}
+}
+
+// 绌烘牸閿篃浼氬垏鎹㈠閫夋
+void CControlJobManagerDlg::OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult)
+{
+	*pResult = 0;
+	auto* p = reinterpret_cast<LPNMTVKEYDOWN>(pNMHDR);
+	if (p->wVKey == VK_SPACE) {
+		HTREEITEM hItem = m_tree.GetSelectedItem();
+		if (hItem) PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
+	}
+}
+
+// 缁熶竴澶勭悊锛堣鏈�缁堢姸鎬� + 浣犵殑涓氬姟锛�
+LRESULT CControlJobManagerDlg::OnAfterTvCheck(WPARAM wParam, LPARAM /*lParam*/)
+{
+	HTREEITEM hItem = (HTREEITEM)wParam;
+	if (!hItem) return 0;
+
+	// 鍙鐞嗙浜屽眰锛氭牴鐨勭洿鎺ュ瓙鑺傜偣锛堝彲閫夛級
+	auto getLevel = [&](HTREEITEM h) {
+		int lv = 0; for (HTREEITEM p = m_tree.GetParentItem(h); p; p = m_tree.GetParentItem(p)) ++lv; return lv;
+	};
+	if (getLevel(hItem) != 1) return 0;
+
+	BOOL checked = m_tree.GetCheck(hItem);
+
+	// 浣犵殑涓氬姟閫昏緫锛堜慨姝d簡 CString::Format 鐨勫弬鏁扮被鍨嬶級
+	auto* pjWarp = reinterpret_cast<PJWarp*>(m_tree.GetItemData(hItem));
+	if (pjWarp) {
+		pjWarp->addToCj = checked;
+	}
+
+	return 0;
+}
+
+void CControlJobManagerDlg::OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult)
+{
+	LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
+
+	bool allow = FALSE;
+	HTREEITEM hOldSel = pNMTreeView->itemOld.hItem;
+	HTREEITEM hSel = pNMTreeView->itemNew.hItem;
+	if (hSel != nullptr) {
+		HTREEITEM hParent = m_tree.GetParentItem(hSel);
+		if (hParent == nullptr) {
+			SERVO::CControlJob* cj = (SERVO::CControlJob*)m_tree.GetItemData(hSel);
+			ASSERT(m_pages.size() == 3);
+			if (0 == ShowPage(2)) {
+				SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_tree.GetItemData(hSel);
+				m_pages[2]->SetContext(pControlJob, 2);
+			}
+		}
+		else if (m_tree.GetParentItem(hParent) == nullptr) {
+			if (0 == ShowPage(1)) {
+				PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hSel);
+				((CCjPage2*)m_pages[1])->SetPjWarps(m_pjWarps);
+				m_pages[1]->SetContext(pjWarp, 1);
+			}
+			else {
+				allow = TRUE;
+			}
+		}
+		else {
+			// 鏈夌鍏� 鈫� 绗笁灞傚強浠ヤ笅 鈫� Glass
+		}
+	}
+
+	*pResult = allow;
+}
+
+int CControlJobManagerDlg::ShowPage(int index)
+{
+	ASSERT(0 <= index && index <= 2);
+
+	for (int i = 0; i < 3; i++) {
+		if (m_pages[i]->IsWindowVisible()) {
+			int ret = m_pages[i]->OnApply();
+			if (ret != 0) return -1;
+		}
+
+		m_pages[i]->ShowWindow(index == i ? SW_SHOW : SW_HIDE);
+	}
+
+	return 0;
+}
+
+void CControlJobManagerDlg::OnDestroy()
+{
+	CDialogEx::OnDestroy();
+	SaveState();
+
+	for (auto page : m_pages) {
+		page->DestroyWindow();
+		delete page;
+	}
+}
+
+void CControlJobManagerDlg::InitData()
+{
+	LoadState();
+	if (m_pControlJob != nullptr) return;
+
+	m_pControlJob = new SERVO::CControlJob("CJ" + CToolUnits::NowStrSec());
+
+	char szBuffer[256];
+	for (int i = 0; i < 4; i++) {
+		sprintf_s(szBuffer, 256, "PJ%03d", i + 1);
+		SERVO::CProcessJob* pj = new SERVO::CProcessJob(std::string(szBuffer));
+		PJWarp pjWarp = {};
+		pjWarp.pj = pj;
+		pjWarp.port = -1;
+		m_pjWarps.push_back(pjWarp);
+	}
+}
+
+void CControlJobManagerDlg::OnBnClickedButtonApply()
+{
+	for (auto item : m_pages) {
+		if (item->IsWindowVisible()) {
+			if (0 == item->OnApply()) {
+				GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
+			}
+		}
+	}
+}
+
+void CControlJobManagerDlg::UpProcessJobId(PJWarp* pjWarp)
+{
+	// 鏇存柊鏍戞帶浠�
+	// 閬嶅巻鏍硅妭鐐�
+	HTREEITEM hRoot = m_tree.GetRootItem();
+	while (hRoot) {
+		// 閬嶅巻绗簩灞傚瓙鑺傜偣
+		HTREEITEM hChild = m_tree.GetChildItem(hRoot);
+		while (hChild) {
+			DWORD_PTR data = m_tree.GetItemData(hChild);
+			if ((void*)data == pjWarp) {
+				SERVO::CProcessJob* pj = (SERVO::CProcessJob*)pjWarp->pj;
+				m_tree.SetItemText(hChild, pj->id().c_str());
+				return; // 鎵惧埌灏辫繑鍥�
+			}
+			hChild = m_tree.GetNextSiblingItem(hChild);
+		}
+
+		hRoot = m_tree.GetNextSiblingItem(hRoot);
+	}
+}
+
+void CControlJobManagerDlg::UpControlJobId(SERVO::CControlJob* pControlJob)
+{
+	// 鏇存柊鏍戞帶浠�
+	// 閬嶅巻鏍硅妭鐐�
+	HTREEITEM hRoot = m_tree.GetRootItem();
+	if (hRoot != nullptr) {
+		DWORD_PTR data = m_tree.GetItemData(hRoot);
+		if ((void*)data == pControlJob) {
+			m_tree.SetItemText(hRoot, pControlJob->id().c_str());
+			return; // 鎵惧埌灏辫繑鍥�
+		}
+	}
+}
+
+void CControlJobManagerDlg::LoadState()
+{
+	if (!m_bHasState) return;
+
+	// 鎶� s_state -> 鎴愬憳鍙橀噺
+	m_pControlJob = m_state.pControlJob;
+	m_pjWarps = m_state.pjWarps;
+}
+
+void CControlJobManagerDlg::SaveState()
+{
+	m_state.pControlJob = m_pControlJob;
+	m_state.pjWarps = m_pjWarps;
+	m_bHasState = true;
+}
+
+void CControlJobManagerDlg::OnBnClickedButtonBathCompletion()
+{
+	// 鍏堟鏌ュ綋鍓峬aster
+	auto& master = theApp.m_model.getMaster();
+	if (!master.canCreateControlJob()) {
+		AfxMessageBox("褰撳墠Master鏈夋湭缁撴壒鐨凧ob, 璇峰厛缁撴壒澶勭悊");
+		return;
+	}
+
+
+	// 鍏堝簲鐢�
+	for (int i = 0; i < 3; i++) {
+		if (m_pages[i]->IsWindowVisible()) {
+			int ret = m_pages[i]->OnApply();
+			if (ret != 0) return ;
+		}
+	}
+	GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
+
+
+	// 鍏堟鏌ユ暟鎹纭��
+	int checkCount = 0;
+	for (auto item : m_pjWarps) {
+		if (!item.addToCj) continue;
+		checkCount++;
+	}
+	if (checkCount == 0) {
+		AfxMessageBox(_T("鎮ㄦ病鏈夐�夋嫨瑕佽繘琛屽伐鑹哄鐞嗙殑Process Job!\n璇峰湪瑕佽繘琛屽伐鑹哄鐞嗙殑Process Job鍓嶆墦鍕俱��"));
+		return;
+	}
+
+
+	SERVO::CLoadPort* pPorts[4];
+	pPorts[0] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT1);
+	pPorts[1] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT2);
+	pPorts[2] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT3);
+	pPorts[3] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT4);
+
+	bool bProcessStart[] = {false, false, false, false};
+	std::vector<SERVO::CProcessJob*> pjs;
+	for (auto item : m_pjWarps) {
+		if (!item.addToCj) continue;
+		if (item.port == -1) continue;
+		BOOL bCheck = FALSE;
+		for (int i = 0; i < 8; i++) {
+			if (item.checkSlot[i]) {
+				bCheck = TRUE;
+				break;
+			}
+		}
+		if (!bCheck) continue;
+
+		SERVO::CProcessJob* pScr = (SERVO::CProcessJob*)item.pj;
+		pScr->setPjWarp(item);
+		pScr->setLotId("LotID1");
+		pScr->setProductId("ProductId1");
+		pScr->setOperationId("OperationId");
+		pScr->setRecipe(SERVO::RecipeMethod::NoTuning, pScr->recipeSpec());
+
+		SERVO::CProcessJob * pj = new SERVO::CProcessJob(pScr->id());
+		pj->setPjWarp(item);
+		pj->setLotId("LotID1");
+		pj->setProductId("ProductId1");
+		pj->setOperationId("OperationId");
+		pj->setRecipe(SERVO::RecipeMethod::NoTuning, pScr->recipeSpec());
+
+		std::vector<SERVO::CarrierSlotInfo> carriers;
+		SERVO::CarrierSlotInfo csi;
+		csi.carrierId = pPorts[item.port]->getCassetteId();
+		for (int i = 0; i < 8; i++) {
+			if (item.checkSlot[i]) {
+				SERVO::CGlass* pGlass = pPorts[item.port]->getGlassFromSlot(i+1);
+				if (pGlass != nullptr) {
+					csi.slots.push_back(i + 1);
+				}
+			}
+		}
+		carriers.push_back(csi);
+		pj->setCarriers(carriers);
+		pjs.push_back(pj);
+		bProcessStart[item.port] = true;
+
+		m_pControlJob->addPJ(pScr->id());
+	}
+
+
+	if (pjs.empty()) {
+		AfxMessageBox(_T("娌℃湁闇�瑕佽繘琛屽伐鑹哄鐞嗙殑Process Job!\n鍙兘鏈�夋嫨Port鎴栭�夋嫨浠讳綍鐗╂枡銆�"));
+		return;
+	}
+
+	m_pControlJob->setPJs(pjs);
+	m_pControlJob->clearIssues();
+	int nRet = master.setProcessJobs(pjs);
+
+	// 娌℃湁闂鐨刾j瑕侀噴鏀�
+	for (auto pj : pjs) {
+		if (!pj->issues().empty()) {
+			delete pj;
+		}
+	}
+	pjs.clear();
+
+	if (nRet <= 0) {
+		std::string msg("鍚屾Process Job澶辫触!");
+		for (auto pj : pjs) {
+			auto& issues = pj->issues();
+			if (!issues.empty()) {
+				msg.append("\n");
+				msg.append(pj->id());
+				msg.append(":\n");
+				for (auto i : issues) {
+					msg.append("[");
+					msg.append(std::to_string(i.code));
+					msg.append("]");
+					msg.append(i.text);
+					msg.append("\n");
+				}
+			}
+		}
+		AfxMessageBox(msg.c_str());
+
+		return;
+	}
+
+	nRet = master.setControlJob(*m_pControlJob);
+	if (nRet != 0) {
+		std::string msg("鍚屾ControlJob澶辫触!");
+		auto& issues = m_pControlJob->issues();
+		if (!issues.empty()) {
+			msg.append("\n");
+			for (auto i : issues) {
+				msg.append("[");
+				msg.append(std::to_string(i.code));
+				msg.append("]");
+				msg.append(i.text);
+				msg.append("\n");
+			}
+		}
+		AfxMessageBox(msg.c_str());
+		return;
+	}
+
+
+	// 鎴愬姛锛岃鍒ゆ柇锛屽悓姝ュ埌slot鐨刧lass涓紝绫诲瀷绛�
+	for (int p = 0; p < 4; p++) {
+		if (m_pjWarps[p].port == -1) continue;
+		ASSERT(0 <= m_pjWarps[p].port && m_pjWarps[p].port <= 3);
+
+		SERVO::CLoadPort* pLoadPort = pPorts[m_pjWarps[p].port];
+		for (int i = 0; i < SLOT_MAX; ++i) {
+			SERVO::CSlot* pSlot = pLoadPort->getSlot(i);
+			if (!pSlot) {
+				continue;
+			}
+			
+			// 璁剧疆 Panel ID 鍜屽嬀閫夋
+			SERVO::CProcessJob* pj = (SERVO::CProcessJob*)m_pjWarps[p].pj;
+			int nRecipeID = RecipeManager::getInstance().getIdByPPID(pj->recipeSpec());
+			RecipeInfo stRecipeInfo = RecipeManager::getInstance().getRecipeByPPID(pj->recipeSpec());
+			std::vector<DeviceRecipe> vecRecipeInfo = stRecipeInfo.vecDeviceList;
+			SERVO::CGlass* pGlass = dynamic_cast<SERVO::CGlass*>(pSlot->getContext());
+			SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
+			if (pGlass != nullptr && pJobDataS != nullptr) {
+				pGlass->setScheduledForProcessing(m_pjWarps[p].checkSlot[i]);
+				pGlass->setType(static_cast<SERVO::MaterialsType>(m_pjWarps[p].material[i]));
+
+				SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
+				pJobDataS->setLotId(pj->getLotId().c_str());
+				pJobDataS->setProductId(pj->getProductId().c_str());
+				pJobDataS->setOperationId(pj->getOperationId().c_str());
+				pJobDataS->setMaterialsType(m_pjWarps[p].material[i]);
+				pJobDataS->setMasterRecipe(nRecipeID);
+
+				for (const auto& info : vecRecipeInfo) {
+					const std::string& name = info.strDeviceName;
+					short nRecipeID = (short)info.nRecipeID;
+				
+					if (name == EQ_NAME_EFEM) {
+						pJobDataS->setDeviceRecipeId(0, nRecipeID);
+					}
+					else if (name == EQ_NAME_BONDER1) {
+						pJobDataS->setDeviceRecipeId(1, nRecipeID);
+					}
+					else if (name == EQ_NAME_BONDER2) {
+						pJobDataS->setDeviceRecipeId(2, nRecipeID);
+					}
+					else if (name == EQ_NAME_BAKE_COOLING) {
+						pJobDataS->setDeviceRecipeId(3, nRecipeID);
+					}
+					else if (name == EQ_NAME_VACUUMBAKE) {
+						pJobDataS->setDeviceRecipeId(4, nRecipeID);
+					}
+					else if (name == EQ_NAME_MEASUREMENT) {
+						pJobDataS->setDeviceRecipeId(5, nRecipeID);
+					}
+				}
+			}
+		}
+	}
+
+
+	// process start
+	for (int p = 0; p < 4; p++) {
+		if (bProcessStart[p]) {
+			pPorts[p]->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
+			Sleep(100);
+		}
+	}
+}
diff --git a/SourceCode/Bond/Servo/CControlJobManagerDlg.h b/SourceCode/Bond/Servo/CControlJobManagerDlg.h
new file mode 100644
index 0000000..031ed7c
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJobManagerDlg.h
@@ -0,0 +1,69 @@
+锘�#pragma once
+#include "CCjPage1.h"
+#include "CCjPage2.h"
+#include "CCjPage3.h"
+#include "ApredTreeCtrl2.h"
+
+
+#define WM_AFTER_TVCHECK (WM_USER + 1000)
+
+// CControlJobManagerDlg 瀵硅瘽妗�
+
+class CControlJobManagerDlg : public CDialogEx
+{
+	DECLARE_DYNAMIC(CControlJobManagerDlg)
+
+public:
+	CControlJobManagerDlg(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CControlJobManagerDlg();
+	static void FreeState();
+
+private:
+	void Resize();
+	void UpdateCtrlState();
+	void UpdateControlJob();
+	bool AddPorcessJob(SERVO::CProcessJob* pj);
+	bool RemovePorcessJob(SERVO::CProcessJob* pj);
+	void UpProcessJobId(PJWarp* pjWarp);
+	void UpControlJobId(SERVO::CControlJob* pControlJob);
+	void InitData();
+	void LoadState();
+	void SaveState();
+	int ShowPage(int index);
+
+private:
+	std::vector<CCjPageBase*> m_pages;
+	SERVO::CControlJob* m_pControlJob;
+	std::vector<PJWarp> m_pjWarps;
+	CApredTreeCtrl2 m_tree;
+
+public:
+	struct State {
+		SERVO::CControlJob* pControlJob = nullptr;
+		std::vector<PJWarp> pjWarps;
+	};
+	static bool m_bHasState;
+	static State m_state;
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_DIALOG_CONTROL_JOB_MANAGER };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnSize(UINT nType, int cx, int cy);
+	afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI);
+	afx_msg void OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg void OnDestroy();
+	afx_msg void OnBnClickedButtonApply();
+	afx_msg void OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg void OnBnClickedButtonBathCompletion();
+	afx_msg void OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg void OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult);
+	afx_msg LRESULT OnAfterTvCheck(WPARAM wParam, LPARAM lParam);
+};
diff --git a/SourceCode/Bond/Servo/CEquipment.cpp b/SourceCode/Bond/Servo/CEquipment.cpp
index 6a3b56d..f9d930a 100644
--- a/SourceCode/Bond/Servo/CEquipment.cpp
+++ b/SourceCode/Bond/Servo/CEquipment.cpp
@@ -26,7 +26,6 @@
 		m_pCclink = nullptr;
 		m_nBaseAlarmId = 0;
 		m_pArm = nullptr;
-		m_processState = PROCESS_STATE::Ready;
 		m_blockReadBit = { 0 };
 		m_nTestFlag = 0;
 		InitializeCriticalSection(&m_criticalSection);
@@ -144,13 +143,15 @@
 		return 0;
 	}
 
-	void CEquipment::setProcessState(PROCESS_STATE state)
+	void CEquipment::setProcessState(int nSlotNo, PROCESS_STATE state)
 	{
-		m_processState = state;
-		onProcessStateChanged(m_processState);
+		if (nSlotNo <= 0 || nSlotNo > 8) return;
+
+		m_processState[nSlotNo - 1] = state;
+		onProcessStateChanged(nSlotNo, m_processState[nSlotNo - 1]);
 
 		if (m_listener.onProcessStateChanged != nullptr) {
-			m_listener.onProcessStateChanged(this, m_processState);
+			m_listener.onProcessStateChanged(this, nSlotNo, m_processState[nSlotNo - 1]);
 		}
 	}
 
@@ -909,8 +910,8 @@
 		Unlock();
 
 
-		if (m_processState != PROCESS_STATE::Ready) {
-			setProcessState(PROCESS_STATE::Ready);
+		if (m_processState[port] != PROCESS_STATE::Ready) {
+			setProcessState(port, PROCESS_STATE::Ready);
 		}
 
 		if (m_listener.onDataChanged != nullptr) {
@@ -941,9 +942,11 @@
 		pGlass->release();				// tempFetchOut需要调用一次release
 		Unlock();
 
+		/*
 		if (m_processState != PROCESS_STATE::Processing) {
 			setProcessState(PROCESS_STATE::Processing);
 		}
+		*/
 
 		if (m_listener.onDataChanged != nullptr) {
 			m_listener.onDataChanged(this, EDCC_STORED_JOB);
@@ -1595,7 +1598,7 @@
 
 		auto rawData = processData.getParamsRawData();
 		std::vector<CParam> tempParams;
-		this->parsingParams((const char*)rawData.data(), rawData.size(), tempParams);
+		this->parsingProcessData((const char*)rawData.data(), rawData.size(), tempParams);
 		int n = processData.getTotalParameter();
 		std::vector<CParam> params(tempParams.begin(), tempParams.begin() + min(n, (int)tempParams.size()));
 		pGlass->addParams(params);
@@ -1929,12 +1932,17 @@
 			year, month, day, hour, minute, second
 			);
 
+		CGlass* pGlass = getGlassFromSlot(slotNo);
+		if (pGlass == nullptr) {
+			LOGE("<CEquipment-%s>decodeJobProcessStartReport, 找不到对应glass", getName().c_str());
+		}
+		if (slotNo <= 0 || slotNo > 8) return -1;
 
-		if (m_processState != PROCESS_STATE::Processing) {
+		if (m_processState[slotNo -1] != PROCESS_STATE::Processing) {
 			Lock();
 			m_svDatas.clear();
 			Unlock();
-			setProcessState(PROCESS_STATE::Processing);
+			setProcessState(slotNo, PROCESS_STATE::Processing);
 		}
 
 
@@ -2002,11 +2010,11 @@
 		);
 
 
-		if (m_processState != PROCESS_STATE::Complete) {
-			setProcessState(PROCESS_STATE::Complete);
-		}
-		
 		CGlass* pGlass = getGlassFromSlot(slotNo);
+		if (m_processState[slotNo - 1] != PROCESS_STATE::Complete) {
+			setProcessState(slotNo, PROCESS_STATE::Complete);
+		}
+	
 		if (pGlass == nullptr) {
 			LOGE("<CEquipment-%s>decodeJobProcessEndReport, 找不到对应glass", getName().c_str());
 		}
@@ -2015,9 +2023,6 @@
 			if (pJs->getCassetteSequenceNo() == cassetteNo
 				&& pJs->getJobSequenceNo() == jobSequenceNo) {
 				pGlass->processEnd(m_nID, getSlotUnit(slotNo));
-				if (m_processState != PROCESS_STATE::Complete) {
-					setProcessState(PROCESS_STATE::Complete);
-				}
 			}
 			else {
 				LOGE("<CEquipment-%s>decodeJobProcessEndReport, jobSequenceNo或jobSequenceNo不匹配",
@@ -2136,7 +2141,7 @@
 		return 0;
 	}
 
-	int CEquipment::onProcessStateChanged(PROCESS_STATE state)
+	int CEquipment::onProcessStateChanged(int nSlotNo, PROCESS_STATE state)
 	{
 		return 0;
 	}
@@ -2237,7 +2242,7 @@
 				return -1;
 			});
 		pStep->setName(STEP_EQ_FAC_DATA_REPORT);
-		pStep->setProp("Port", (void*)port);
+		pStep->setProp("Port", (void*)(__int64)port);
 		pStep->setWriteSignalDev(writeSignalDev);
 		if (addStep(STEP_ID_FAC_DATA_REPORT, pStep) != 0) {
 			delete pStep;
diff --git a/SourceCode/Bond/Servo/CEquipment.h b/SourceCode/Bond/Servo/CEquipment.h
index ff16693..36a05c9 100644
--- a/SourceCode/Bond/Servo/CEquipment.h
+++ b/SourceCode/Bond/Servo/CEquipment.h
@@ -55,7 +55,7 @@
 	typedef std::function<void(void* pEiuipment, void* pReport)> ONVCREVENTREPORT;
 	typedef std::function<BOOL(void* pEiuipment, int port, CJobDataB* pJobDataB)> ONPREFETCHEDOUTJOB;
 	typedef std::function<BOOL(void* pEiuipment, int port, CJobDataB* pJobDataB, short& putSlot)> ONPRESTOREDJOB;
-	typedef std::function<void(void* pEiuipment, PROCESS_STATE state)> ONPROCESSSTATE;
+	typedef std::function<void(void* pEiuipment, int nSlotNo, PROCESS_STATE state)> ONPROCESSSTATE;
 	typedef std::function<void(void* pEiuipment, short scanMap, short downMap)> ONMAPMISMATCH;
 	typedef std::function<void(void* pEiuipment, short status, __int64 data)> ONPORTSTATUSCHANGED;
 	
@@ -140,7 +140,7 @@
 		virtual int onProcessData(CProcessData* pProcessData);
 		virtual int onSendAble(int port);
 		virtual int onReceiveAble(int port);
-		virtual int onProcessStateChanged(PROCESS_STATE state);
+		virtual int onProcessStateChanged(int nSlotNo, PROCESS_STATE state);
 		virtual int getIndexerOperationModeBaseValue();
 		virtual bool isSlotProcessed(int slot) { return true; };
 		bool isAlarmStep(SERVO::CStep* pStep);
@@ -267,7 +267,7 @@
 		int decodeJobProcessStartReport(CStep* pStep, const char* pszData, size_t size);
 		int decodeJobProcessEndReport(CStep* pStep, const char* pszData, size_t size);
 		BOOL compareJobData(CJobDataB* pJobDataB, CJobDataS* pJobDataS);
-		void setProcessState(PROCESS_STATE state);
+		void setProcessState(int nSlotNo, PROCESS_STATE state);
 		float toFloat(const char* pszAddr);
 
 	protected:
@@ -307,7 +307,7 @@
 		int m_nBaseAlarmId;
 		CRecipesManager m_recipesManager;
 		CSlot m_slot[SLOT_MAX];
-		PROCESS_STATE m_processState;
+		PROCESS_STATE m_processState[SLOT_MAX] = { PROCESS_STATE::Ready };
 		std::vector<SERVO::CSVData> m_svDatas;
 
 	private:
diff --git a/SourceCode/Bond/Servo/CExpandableListCtrl.cpp b/SourceCode/Bond/Servo/CExpandableListCtrl.cpp
index ef21ed0..39831cc 100644
--- a/SourceCode/Bond/Servo/CExpandableListCtrl.cpp
+++ b/SourceCode/Bond/Servo/CExpandableListCtrl.cpp
@@ -95,26 +95,140 @@
     m_rowColors.resize(GetItemCount());
 
     SetRedraw(TRUE);
-    Invalidate();
+    Invalidate(FALSE);
 }
 
+// 鈥斺�� 浼樺寲鍚庣殑灞曞紑/鏀惰捣锛氬眬閮ㄦ彃鍏�/鍒犻櫎锛屼笉鍏ㄩ噺 RebuildVisible 鈥斺�� //
 void CExpandableListCtrl::Expand(Node* n)
 {
     if (!n || n->children.empty()) return;
-    if (!n->expanded) { n->expanded = true; RebuildVisible(); }
+    if (n->expanded) return;
+
+    // 鏈湴宸ュ叿锛氭壘鑺傜偣鍦� m_visible 涓殑琛屽彿
+    auto VisibleIndexOf = [&](Node* x)->int {
+        for (int i = 0; i < (int)m_visible.size(); ++i)
+            if (m_visible[i] == x) return i;
+        return -1;
+    };
+    // 閫掑綊鏀堕泦鈥滃簲褰撳彲瑙佲�濈殑瀛愭爲锛堝彈 expanded 褰卞搷锛�
+    std::vector<Node*> toInsert;
+    std::function<void(Node*)> CollectExpandedSubtree = [&](Node* x) {
+        if (!x) return;
+        for (auto& up : x->children) {
+            Node* ch = up.get();
+            toInsert.push_back(ch);
+            if (ch->expanded && !ch->children.empty())
+                CollectExpandedSubtree(ch);
+        }
+    };
+    // 浠� pos 璧锋彃鍏� nodes锛屽榻� m_visible / ListCtrl / m_rowColors
+    auto InsertRowsAt = [&](int pos, const std::vector<Node*>& nodes) {
+        if (nodes.empty()) return;
+        const int colCount = GetHeaderCtrl() ? GetHeaderCtrl()->GetItemCount() : 1;
+
+        SetRedraw(FALSE);
+
+        // 1) 鍏堟彃 m_visible
+        m_visible.insert(m_visible.begin() + pos, nodes.begin(), nodes.end());
+
+        // 2) 鍐嶆彃 ListCtrl
+        for (int i = 0; i < (int)nodes.size(); ++i) {
+            Node* cur = nodes[i];
+            LVITEM lvi{}; lvi.mask = LVIF_TEXT;
+            lvi.iItem = pos + i;
+            lvi.iSubItem = 0;
+            lvi.pszText = const_cast<LPTSTR>((LPCTSTR)(cur->cols.empty() ? _T("") : cur->cols[0]));
+            InsertItem(&lvi);
+
+            for (int col = 1; col < colCount; ++col) {
+                CString txt = (col < (int)cur->cols.size()) ? cur->cols[col] : _T("");
+                SetItemText(pos + i, col, txt);
+            }
+        }
+
+        // 3) 琛屽彿棰滆壊鏁扮粍鍚屾鎻掑叆榛樿鑹�
+        m_rowColors.insert(m_rowColors.begin() + pos, nodes.size(), RowColor{});
+
+        SetRedraw(TRUE);
+        Invalidate(FALSE);
+    };
+
+    // 鈥斺�� 鏍囪灞曞紑
+    n->expanded = true;
+
+    // 鈥斺�� 鍦� UI 閲屾彃鍏ュ叾鈥滃簲褰撳彲瑙佲�濈殑瀛愭爲
+    const int pos = VisibleIndexOf(n);
+    if (pos < 0) { RebuildVisible(); return; }
+
+    CollectExpandedSubtree(n);
+    InsertRowsAt(pos + 1, toInsert);
 }
 
 void CExpandableListCtrl::Collapse(Node* n)
 {
     if (!n || n->children.empty()) return;
-    if (n->expanded) { n->expanded = false; RebuildVisible(); }
+    if (!n->expanded) return;
+
+    // 鏈湴宸ュ叿锛氭壘鑺傜偣琛屽彿
+    auto VisibleIndexOf = [&](Node* x)->int {
+        for (int i = 0; i < (int)m_visible.size(); ++i)
+            if (m_visible[i] == x) return i;
+        return -1;
+    };
+    // 璁$畻鈥滃綋鍓嶅彲瑙佺殑鎵�鏈夊悗浠f暟閲忊�濓紙鍩轰簬 level 閫掑噺鍒ゆ柇锛�
+    auto CountDescendantsInVisible = [&](Node* x)->int {
+        if (!x) return 0;
+        const int start = VisibleIndexOf(x);
+        if (start < 0) return 0;
+        const int baseLevel = x->level;
+        int cnt = 0;
+        for (int i = start + 1; i < (int)m_visible.size(); ++i) {
+            if (!m_visible[i]) break;
+            if (m_visible[i]->level <= baseLevel) break;
+            ++cnt;
+        }
+        return cnt;
+    };
+    // 浠� UI 鍒犻櫎 pos 寮�濮嬬殑 count 琛岋紝骞跺悓姝� m_visible/m_rowColors
+    auto DeleteRowsAt = [&](int pos, int count) {
+        if (count <= 0) return;
+
+        SetRedraw(FALSE);
+
+        // 鍒� ListCtrl锛氫竴鐩村垹 pos锛屽洜涓哄垹涓�琛屽悗鍚庣画涓婄Щ
+        for (int i = 0; i < count; ++i) {
+            DeleteItem(pos);
+        }
+        // 鍒� m_visible
+        m_visible.erase(m_visible.begin() + pos, m_visible.begin() + pos + count);
+        // 鍒犻鑹�
+        if (pos >= 0 && pos <= (int)m_rowColors.size()) {
+            int end = min((int)m_rowColors.size(), pos + count);
+            m_rowColors.erase(m_rowColors.begin() + pos, m_rowColors.begin() + end);
+        }
+
+        SetRedraw(TRUE);
+        Invalidate(FALSE);
+    };
+
+    // 鈥斺�� 鏍囪鏀惰捣
+    n->expanded = false;
+
+    // 鈥斺�� 鍙垹闄ゅ叾鈥滃綋鍓嶅彲瑙佲�濈殑鎵�鏈夊悗浠�
+    const int pos = VisibleIndexOf(n);
+    if (pos < 0) { RebuildVisible(); return; }
+
+    const int cnt = CountDescendantsInVisible(n);
+    if (cnt > 0) {
+        DeleteRowsAt(pos + 1, cnt);
+    }
 }
 
 void CExpandableListCtrl::Toggle(Node* n)
 {
     if (!n || n->children.empty()) return;
-    n->expanded = !n->expanded;
-    RebuildVisible();
+    if (n->expanded) Collapse(n);
+    else             Expand(n);
 }
 
 CExpandableListCtrl::Node* CExpandableListCtrl::GetNodeByVisibleIndex(int i) const
@@ -136,7 +250,6 @@
     for (int i = 0; i < (int)m_visible.size(); ++i) {
         if (m_visible[i] == n) {
             RedrawItems(i, i);
-            UpdateWindow();
             return;
         }
     }
@@ -151,7 +264,6 @@
         for (int i = 0; i < (int)m_visible.size(); ++i) {
             if (m_visible[i] == n) {
                 RedrawItems(i, i);
-                UpdateWindow();
                 return;
             }
         }
@@ -183,7 +295,6 @@
     m_rowColors[row] = rc;
 
     RedrawItems(row, row);
-    UpdateWindow();
 }
 
 CRect CExpandableListCtrl::expanderRectForRow(int row) const
@@ -256,11 +367,11 @@
 
     // 鈥斺�� 鑻ョ偣鍑诲埌闇�瑕佲�滃叏鏂囨樉绀衡�濈殑鍒楋紝鍒欏悜鐖剁獥鍙e彂閫佽嚜瀹氫箟閫氱煡 鈥斺�� //
     if (!m_popupCols.empty()) {
-        LPNMITEMACTIVATE pia = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
+        LPNMITEMACTIVATE pia2 = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
 
         // 鐢� SubItemHitTest 鏇寸簿鍑嗘嬁鍒板垪
         LVHITTESTINFO ht{};
-        ht.pt = pia->ptAction;
+        ht.pt = pia2->ptAction;
         int hit = SubItemHitTest(&ht);
         if (hit >= 0 && ht.iItem >= 0 && ht.iSubItem >= 0) {
             const int row = ht.iItem;
@@ -474,7 +585,7 @@
     DeleteAllItems();
     SetRedraw(TRUE);
 
-    Invalidate();
+    Invalidate(FALSE);
 }
 
 void CExpandableListCtrl::SetPopupFullTextColumns(const std::vector<int>& cols)
@@ -501,5 +612,3 @@
     const int kPadding = 8; // 棰勭暀涓�鐐硅竟璺�/鐪佺暐鍙蜂綑閲�
     return sz.cx > (rcCell.Width() - kPadding);
 }
-
-
diff --git a/SourceCode/Bond/Servo/CGlass.cpp b/SourceCode/Bond/Servo/CGlass.cpp
index d1c7a94..ae2bca0 100644
--- a/SourceCode/Bond/Servo/CGlass.cpp
+++ b/SourceCode/Bond/Servo/CGlass.cpp
@@ -1,5 +1,6 @@
 #include "stdafx.h"
 #include "CGlass.h"
+#include "Log.h"
 
 
 namespace SERVO {
@@ -305,7 +306,7 @@
 			return "Queued";
 			break;
 		case SERVO::GlsState::Completed:
-			return "Queued";
+			return "Completed";
 			break;
 		case SERVO::GlsState::Aborted:
 			return "Aborted";
@@ -430,4 +431,147 @@
 
 		return strOut;
 	}
+
+	// ========== SV数据管理接口实现 ==========
+
+	void CGlass::addSVData(int machineId, const std::string& dataType, const SVDataItem& dataItem) {
+		m_svDatas[machineId][dataType].push_back(dataItem);
+	}
+
+	void CGlass::addSVData(int machineId, const std::string& dataType, double value) {
+		auto now = std::chrono::system_clock::now();
+		m_svDatas[machineId][dataType].emplace_back(now, value);
+	}
+
+	void CGlass::addSVData(int machineId, const std::string& dataType, int64_t timestamp, double value) {
+		// 将int64_t时间戳转换为system_clock::time_point
+		std::chrono::system_clock::time_point timePoint{
+			std::chrono::milliseconds(timestamp)  // 假设timestamp是毫秒
+			// 如果是秒,使用:std::chrono::seconds(timestamp)
+		};
+		m_svDatas[machineId][dataType].emplace_back(timePoint, value);
+	}
+
+	void CGlass::addSVData(int machineId, const std::string& dataType, const std::vector<SVDataItem>& dataItems) {
+		auto& dataList = m_svDatas[machineId][dataType];
+		dataList.insert(dataList.end(), dataItems.begin(), dataItems.end());
+	}
+
+	std::vector<SVDataItem> CGlass::getSVData(int machineId, const std::string& dataType) const {
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			auto dataIt = machineIt->second.find(dataType);
+			if (dataIt != machineIt->second.end()) {
+				return dataIt->second;
+			}
+		}
+		return std::vector<SVDataItem>();
+	}
+
+	std::vector<std::string> CGlass::getSVDataTypes(int machineId) const {
+		std::vector<std::string> types;
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			for (const auto& pair : machineIt->second) {
+				types.push_back(pair.first);
+			}
+		}
+		return types;
+	}
+
+	std::unordered_map<std::string, std::vector<SVDataItem>> CGlass::getMachineSVData(int machineId) const {
+		auto it = m_svDatas.find(machineId);
+		if (it != m_svDatas.end()) {
+			return it->second;
+		}
+		return std::unordered_map<std::string, std::vector<SVDataItem>>();
+	}
+
+	const std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>>& CGlass::getAllSVData() const {
+		return m_svDatas;
+	}
+
+	bool CGlass::hasSVData(int machineId, const std::string& dataType) const {
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			return machineIt->second.find(dataType) != machineIt->second.end();
+		}
+		return false;
+	}
+
+	bool CGlass::hasMachineSVData(int machineId) const {
+		return m_svDatas.find(machineId) != m_svDatas.end();
+	}
+
+	std::vector<int> CGlass::getMachineIdsWithSVData() const {
+		std::vector<int> machineIds;
+		for (const auto& pair : m_svDatas) {
+			machineIds.push_back(pair.first);
+		}
+		return machineIds;
+	}
+
+	void CGlass::clearSVData(int machineId, const std::string& dataType) {
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			machineIt->second.erase(dataType);
+			// 如果该机器没有其他数据了,也清除机器条目
+			if (machineIt->second.empty()) {
+				m_svDatas.erase(machineIt);
+			}
+		}
+	}
+
+	void CGlass::clearMachineSVData(int machineId) {
+		m_svDatas.erase(machineId);
+	}
+
+	void CGlass::clearAllSVData() {
+		m_svDatas.clear();
+	}
+
+	size_t CGlass::getSVDataCount(int machineId, const std::string& dataType) const {
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			auto dataIt = machineIt->second.find(dataType);
+			if (dataIt != machineIt->second.end()) {
+				return dataIt->second.size();
+			}
+		}
+		return 0;
+	}
+
+	size_t CGlass::getMachineSVDataCount(int machineId) const {
+		size_t total = 0;
+		auto machineIt = m_svDatas.find(machineId);
+		if (machineIt != m_svDatas.end()) {
+			for (const auto& pair : machineIt->second) {
+				total += pair.second.size();
+			}
+		}
+		return total;
+	}
+
+	size_t CGlass::getTotalSVDataCount() const {
+		size_t total = 0;
+		for (const auto& machinePair : m_svDatas) {
+			for (const auto& dataPair : machinePair.second) {
+				total += dataPair.second.size();
+			}
+		}
+		return total;
+	}
+
+	std::vector<std::pair<int, SVDataItem>> CGlass::findSVDataByType(const std::string& dataType) const {
+		std::vector<std::pair<int, SVDataItem>> result;
+		for (const auto& machinePair : m_svDatas) {
+			auto dataIt = machinePair.second.find(dataType);
+			if (dataIt != machinePair.second.end()) {
+				for (const auto& item : dataIt->second) {
+					result.emplace_back(machinePair.first, item);
+				}
+			}
+		}
+		return result;
+	}
 }
diff --git a/SourceCode/Bond/Servo/CGlass.h b/SourceCode/Bond/Servo/CGlass.h
index 72f304b..79fe83f 100644
--- a/SourceCode/Bond/Servo/CGlass.h
+++ b/SourceCode/Bond/Servo/CGlass.h
@@ -12,6 +12,15 @@
 
 
 namespace SERVO {
+	/// 数据项:时间戳 + 数值
+	struct SVDataItem {
+		std::chrono::system_clock::time_point timestamp;
+		double value;  // 或者根据实际情况使用其他类型
+
+		SVDataItem(std::chrono::system_clock::time_point ts, double val)
+			: timestamp(ts), value(val) {}
+	};
+
 	/// PJ 生命周期(贴近 E40 常见状态)
 	enum class GlsState : uint8_t {
 		NoState = 0,
@@ -100,6 +109,58 @@
 		void addParams(std::vector<CParam>& params);
 		std::vector<CParam>& getParams();
 
+		// ========== SV数据管理接口(新设计)==========
+
+		// 添加数据到指定机器的指定数据类型
+		void addSVData(int machineId, const std::string& dataType, const SVDataItem& dataItem);
+		void addSVData(int machineId, const std::string& dataType, double value); // 自动使用当前时间
+		void addSVData(int machineId, const std::string& dataType, int64_t timestamp, double value);
+
+		// 批量添加数据到指定机器的指定数据类型
+		void addSVData(int machineId, const std::string& dataType, const std::vector<SVDataItem>& dataItems);
+
+		// 获取指定机器的指定数据类型的所有数据
+		std::vector<SVDataItem> getSVData(int machineId, const std::string& dataType) const;
+
+		// 获取指定机器的所有数据类型
+		std::vector<std::string> getSVDataTypes(int machineId) const;
+
+		// 获取指定机器的所有数据(按数据类型组织)
+		std::unordered_map<std::string, std::vector<SVDataItem>> getMachineSVData(int machineId) const;
+
+		// 获取所有机器的数据
+		const std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>>& getAllSVData() const;
+
+		// 检查指定机器是否有指定类型的数据
+		bool hasSVData(int machineId, const std::string& dataType) const;
+
+		// 检查指定机器是否有任何数据
+		bool hasMachineSVData(int machineId) const;
+
+		// 获取所有有SV数据的机器ID
+		std::vector<int> getMachineIdsWithSVData() const;
+
+		// 清空指定机器的指定数据类型的数据
+		void clearSVData(int machineId, const std::string& dataType);
+
+		// 清空指定机器的所有数据
+		void clearMachineSVData(int machineId);
+
+		// 清空所有机器的所有数据
+		void clearAllSVData();
+
+		// 获取指定机器的指定数据类型的数据数量
+		size_t getSVDataCount(int machineId, const std::string& dataType) const;
+
+		// 获取指定机器的总数据数量
+		size_t getMachineSVDataCount(int machineId) const;
+
+		// 获取所有机器的总数据数量
+		size_t getTotalSVDataCount() const;
+
+		// 获取指定数据类型在所有机器中的数据
+		std::vector<std::pair<int, SVDataItem>> findSVDataByType(const std::string& dataType) const;
+
 	private:
 		MaterialsType m_type;
 		std::string m_strID;
@@ -112,6 +173,9 @@
 		BOOL m_bScheduledForProcessing;			/* 是否将加工处理 */
 		CProcessJob* m_pProcessJob;
 		std::vector<CParam> m_params;			// 工艺参数
+
+		// 新的三层数据结构:机器ID -> 数据类型 -> 数据列表
+		std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>> m_svDatas;
 	};
 }
 
diff --git a/SourceCode/Bond/Servo/CJobDataS.cpp b/SourceCode/Bond/Servo/CJobDataS.cpp
index 9f5985b..2231b0b 100644
--- a/SourceCode/Bond/Servo/CJobDataS.cpp
+++ b/SourceCode/Bond/Servo/CJobDataS.cpp
@@ -618,9 +618,9 @@
 		index += sizeof(short);
 
 		memcpy(&m_nQTime[1], &pszBuffer[index], sizeof(short));
-		index += sizeof(int);
+		index += sizeof(short);
 
-		memcpy(&m_nQTime[2], &pszBuffer[index], sizeof(int));
+		memcpy(&m_nQTime[2], &pszBuffer[index], sizeof(short));
 		index += sizeof(short);
 
 		memcpy(&m_nQTimeOverFlag, &pszBuffer[index], sizeof(short));
diff --git a/SourceCode/Bond/Servo/CLoadPort.cpp b/SourceCode/Bond/Servo/CLoadPort.cpp
index 22b517b..ebd1690 100644
--- a/SourceCode/Bond/Servo/CLoadPort.cpp
+++ b/SourceCode/Bond/Servo/CLoadPort.cpp
@@ -353,7 +353,7 @@
 
 		// 从配置读出的enable,初始化时写给efem
 		static int i_enable[4] = { 0 };
-		if ((++i_enable[m_nIndex]) == 10 + m_nIndex) {
+		if ((++i_enable[m_nIndex]) == 20 + m_nIndex) {
 			eablePort(m_bEnable, [&](int code) -> int {
 				LOGI("<LoadPort-%d>eablePort:code=%d", m_nIndex, code);
 				return 0;
@@ -404,7 +404,6 @@
 			m_portStatusReport.serialize(ar);
 		}
 		else {
-			int temp;
 			ar >> m_nIndex;
 			m_portStatusReport.serialize(ar);
 		}
@@ -506,6 +505,9 @@
 	void CLoadPort::setIndex(unsigned int index)
 	{
 		m_nIndex = index;
+
+		std::string id = "Port" + std::to_string(index + 1);
+		m_portStatusReport.setCassetteId(id.c_str());
 	}
 
 	unsigned int CLoadPort::getIndex()
diff --git a/SourceCode/Bond/Servo/CMaster.cpp b/SourceCode/Bond/Servo/CMaster.cpp
index 0925933..d36f540 100644
--- a/SourceCode/Bond/Servo/CMaster.cpp
+++ b/SourceCode/Bond/Servo/CMaster.cpp
@@ -6,9 +6,15 @@
 #include "RecipeManager.h"
 #include <fstream>
 #include "SerializeUtil.h"
+#include "CServoUtilsTool.h"
 
 
 namespace SERVO {
+	static inline int64_t now_ms_epoch() {
+		using namespace std::chrono;
+		return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
+	}
+
 	CMaster* g_pMaster = NULL;
 
 	unsigned __stdcall DispatchThreadFunction(LPVOID lpParam)
@@ -62,6 +68,8 @@
 		m_bBatch = false;
 		m_nContinuousTransferCount = 0;
 		m_nContinuousTransferStep = CTStep_Unknow;
+		m_nContinuousWorkingPort = 0;
+		m_nContinuousWorkingSlot = 0;
 		m_pControlJob = nullptr;
 		m_nTestFlag = 0;
 		InitializeCriticalSection(&m_criticalSection);
@@ -211,10 +219,9 @@
 
 
 
-
-		
 		// 读缓存数据
 		readCache();
+		loadState();
 
 
 		// 定时器
@@ -230,6 +237,10 @@
 		// 监控bit线程
 		m_hReadBitsThreadHandle = (HANDLE)_beginthreadex(NULL, 0, SERVO::ReadBitsThreadFunction, this,
 			0, &m_nReadBitsThreadAddr);
+
+
+		// 曲线服务
+		CreateDAQBridgeServer();
 
 
 		LOGI("<Master>初始化完成.");
@@ -261,6 +272,12 @@
 		}
 		m_listEquipment.clear();
 
+
+		if (m_pCollector != nullptr) {
+			m_pCollector->stopLoop();
+			delete m_pCollector;
+			m_pCollector = nullptr;
+		}
 
 		return 0;
 	}
@@ -804,48 +821,32 @@
 
 			// 批处理模式,最终以此为准,但先保留之前的单片模式
 			else if (m_state == MASTERSTATE::RUNNING_BATCH) {
-				// 首选检查有没有CControlJob, 状态等
-				if (m_pControlJob == nullptr) {
+				// 1) 控制作业生命周期保障
+				if (m_pControlJob == nullptr) { unlock(); continue; }
+				CJState cjst = m_pControlJob->state();
+				if (cjst == CJState::Completed || cjst == CJState::Aborted || cjst == CJState::Failed) {
 					unlock();
 					continue;
 				}
-				CJState state = m_pControlJob->state();
-				if (state == CJState::Completed || state == CJState::Aborted || state == CJState::Failed) {
-					// ConrolJpb已完成
-					LOGE("<Master>ControlJob已经完成或失败中断");
-					unlock();
-					continue;
-				}
-
-
-				if (m_pControlJob->state() == CJState::NoState) {
+				if (cjst == CJState::NoState) {
 					LOGI("<Master>ControlJob已经进入列队");
 					m_pControlJob->queue();
 				}
 				if (m_pControlJob->state() == CJState::Queued) {
 					LOGI("<Master>ControlJob已经启动");
 					m_pControlJob->start();
-
-					if (m_listener.onCjStart != nullptr) {
-						m_listener.onCjStart(this, m_pControlJob);
-					}
+					if (m_listener.onCjStart) m_listener.onCjStart(this, m_pControlJob);
 				}
 				if (m_pControlJob->state() == CJState::Paused) {
 					LOGI("<Master>ControlJob已经恢复运行");
 					m_pControlJob->resume();
 				}
 
-
-				// 如果当前未选择CProcessJob, 选择一个
+				// 2) 若当前无 PJ,则选择一个并上报
 				if (m_inProcesJobs.empty()) {
-					auto pj = acquireNextProcessJob();
-					if (pj != nullptr) {
+					if (auto pj = acquireNextProcessJob()) {
 						m_inProcesJobs.push_back(pj);
-
-						// 这里上报PJ Start事件
-						if (m_listener.onPjStart != nullptr) {
-							m_listener.onPjStart(this, pj);
-						}
+						if (m_listener.onPjStart) m_listener.onPjStart(this, pj);
 					}
 				}
 				if (m_inProcesJobs.empty()) {
@@ -854,54 +855,64 @@
 					continue;
 				}
 
-				// 如果当前没有Glass, 选择
+				// 3) 若队列无 Glass,拉取到等待队列
 				if (m_queueGlasses.empty()) {
 					int nCount = acquireGlassToQueue();
-					LOGI("<Master>已加入 %d 块Glass到工艺列队!", nCount);
+					if (nCount > 0) {
+						LOGI("<Master>已加入 %d 块Glass到工艺列队!", nCount);
+					}
 				}
 
-
-				// 检测判断robot状态
+				// 4) 机器人状态
 				RMDATA& rmd = pEFEM->getRobotMonitoringData();
 				if (rmd.status != ROBOT_STATUS::Idle && rmd.status != ROBOT_STATUS::Run) {
-					unlock();
-					continue;
+					unlock(); continue;
 				}
 
+				// 5) 正在执行的 RobotTask 先让它跑完一拍
 				if (m_pActiveRobotTask != nullptr) {
 					if (m_pActiveRobotTask->isPicked()) {
 						m_pActiveRobotTask->place();
 					}
-					unlock();
-					// 检测到当前有正在下午的任务,确保当前任务完成或中止后继续
-					// LOGI("检测到当前有正在下午的任务,确保当前任务完成或中止后继续...");
+					unlock(); // 等当前任务完成或中止后继续
 					continue;
 				}
 
-
-				// 此处检测优先类型和次要类型(G1或G2)
-				// 如果其中一Bonder有单个玻璃,优先取它的配对类型,否则无所谓了
-				primaryType = MaterialsType::G1;
-				secondaryType = MaterialsType::G2;
-				if ((!pBonder1->canPlaceGlassInSlot(0) && !pBonder1->canPlaceGlassInSlot(1))
-					&& (!pBonder2->canPlaceGlassInSlot(0) && !pBonder2->canPlaceGlassInSlot(1))) {
-					// 如果G1和G2都满了,那就看Aligner, 如果Aligner有玻璃为G1, 则取G2
-					CGlass* pGlass = pAligner->getGlassFromSlot(1);
-					if (pGlass != nullptr && pGlass->getType() == MaterialsType::G1) {
-						primaryType = MaterialsType::G2;
-						secondaryType = MaterialsType::G1;
+				// 6) ——关键:全局统计 G1/G2 与组数门限(与单片分支对齐)——
+				auto countG1G2 = [&]() {
+					int g1 = 0, g2 = 0;
+					if (pBonder1->slotHasGlass(0)) g2++;
+					if (pBonder1->slotHasGlass(1)) g1++;
+					if (pBonder2->slotHasGlass(0)) g2++;
+					if (pBonder2->slotHasGlass(1)) g1++;
+					if (pFliper->slotHasGlass(0))  g2++;
+					if (pVacuumBake->slotHasGlass(0)) g1++;
+					if (pVacuumBake->slotHasGlass(1)) g1++;
+					if (auto g = pAligner->getGlassFromSlot(0)) {
+						auto t = g->getType();
+						if (t == MaterialsType::G1) g1++; else if (t == MaterialsType::G2) g2++;
 					}
-				}
-				else if ((pBonder1->canPlaceGlassInSlot(0) && !pBonder1->canPlaceGlassInSlot(1))
-					|| (pBonder2->canPlaceGlassInSlot(0) && !pBonder2->canPlaceGlassInSlot(1))) {
-					primaryType = MaterialsType::G2;
-					secondaryType = MaterialsType::G1;
-				}
+					return std::pair<int, int>(g1, g2);
+				};
 
+				int g1Count = 0, g2Count = 0;
+				std::tie(g1Count, g2Count) = countG1G2();
+				int nGlassGroup = min(g1Count, g2Count);
+				int nExtraType = (g1Count == g2Count ? 0 : (g1Count > g2Count ? 1 : 2));
 
-				// Measurement -> LoadPort
+				// primary/secondary 统一定义(secondary 默认 G0)
+				MaterialsType primaryType = MaterialsType::G1;
+				MaterialsType secondaryType = MaterialsType::G0;
+				if (nExtraType == 0) primaryType = MaterialsType::G2; // 与单片分支一致
+				else                 primaryType = MaterialsType::G1;
+
+				// 组数门限:≥2 组时不再从 LP 上片,避免堆积(与单片一致)
+				bool blockLoadFromLP = (nGlassGroup >= 2);
+
+				// 7) Measurement -> LoadPort(固定:G1 优先回 LP)
 				if (rmd.armState[0] || rmd.armState[1]) {
-					LOGD("Arm1 %s, Arm2 %s.", rmd.armState[0] ? _T("不可用") : _T("可用"),
+					LOGD("Arm1 %s, Arm2 %s.",
+						rmd.armState[0] ? _T("不可用") : _T("可用"),
 						rmd.armState[1] ? _T("不可用") : _T("可用"));
 				}
 				for (int s = 0; s < 4; s++) {
@@ -909,140 +920,123 @@
 					if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
 						&& (pt == PortType::Unloading || pt == PortType::Both)
 						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
-						m_pActiveRobotTask = createTransferTask(pMeasurement, pLoadPorts[s], primaryType, secondaryType);
-						if (m_pActiveRobotTask != nullptr) {
-							goto BATCH_PORT_PUT;
-						}
+						m_pActiveRobotTask = createTransferTask(pMeasurement, pLoadPorts[s], MaterialsType::G1, secondaryType);
+						if (m_pActiveRobotTask != nullptr) { goto BATCH_PORT_PUT; }
 					}
 				}
 
-			BATCH_PORT_PUT:
+				BATCH_PORT_PUT:
 				CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 
-
-				// Measurement NG -> LoadPort
-				// NG回原位
+				// 8) Measurement NG -> LoadPort(原位回退)
 				if (!rmd.armState[1]) {
 					m_pActiveRobotTask = createTransferTask_restore(pMeasurement, pLoadPorts);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
-
-				// BakeCooling ->Measurement
+				// 9) BakeCooling -> Measurement
 				if (!rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_bakecooling_to_measurement(pBakeCooling, pMeasurement);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
-
-				// BakeCooling内部
-				// Bake -> Cooling
+				// 10) BakeCooling 内部(Bake -> Cooling)
 				if (!rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_bake_to_cooling(pBakeCooling);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
-
-				// Bonder -> BakeCooling
+				// 11) Bonder -> BakeCooling
 				if (!rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_bonder_to_bakecooling(pBonder1, pBakeCooling);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
-
 				if (!rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_bonder_to_bakecooling(pBonder2, pBakeCooling);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
+				// 12) Fliper(G2) -> Bonder(前置:VacuumBake 有 processed G1;输出 G2 到 Bonder slot0)
+				if (auto pSrcSlot = pVacuumBake->getProcessedSlot(MaterialsType::G1)) {
+					if (!rmd.armState[1] && pBonder1->canPlaceGlassInSlot(0)) {
+						m_pActiveRobotTask = createTransferTask(pFliper, pBonder1, MaterialsType::G2, MaterialsType::G0, 2);
+						CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
+					}
+					if (!rmd.armState[1] && pBonder2->canPlaceGlassInSlot(0)) {
+						m_pActiveRobotTask = createTransferTask(pFliper, pBonder2, MaterialsType::G2, MaterialsType::G0, 2);
+						CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
+					}
+				}
 
-				// Fliper(G2) -> Bonder
-				auto pSrcSlot = pVacuumBake->getProcessedSlot(primaryType);
-				if (pSrcSlot != nullptr && !rmd.armState[1] && !pBonder1->hasBondGlass()) {
-					m_pActiveRobotTask = createTransferTask(pFliper, pBonder1, primaryType, secondaryType, 2);
+				// 13) VacuumBake(G1) -> Bonder(槽级判定:slot0(G2) 已有且 slot1(G1) 为空)
+				if (!rmd.armState[0] && pBonder1->slotHasGlass(0) && !pBonder1->slotHasGlass(1)) {
+					m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder1, MaterialsType::G1, MaterialsType::G0);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
-				if (pSrcSlot != nullptr && !rmd.armState[1] && !pBonder2->hasBondGlass()) {
-					m_pActiveRobotTask = createTransferTask(pFliper, pBonder2, primaryType, secondaryType, 2);
+				if (!rmd.armState[0] && pBonder2->slotHasGlass(0) && !pBonder2->slotHasGlass(1)) {
+					m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder2, MaterialsType::G1, MaterialsType::G0);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
-
-				// VacuumBake(G1) -> Bonder
-				if (!rmd.armState[0] && !pBonder1->hasBondGlass()) {
-					m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder1, primaryType, secondaryType);
-					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
-				}
-
-				if (!rmd.armState[0] && !pBonder2->hasBondGlass()) {
-					m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder2, primaryType, secondaryType);
-					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
-				}
-
-
-				// Aligner -> Fliper(G2)
-				// Aligner -> VacuumBake(G1)
+				// 14) Aligner -> Fliper(G2) 以及 -> VacuumBake(G1)(固定映射)
 				if (!rmd.armState[1]) {
-					m_pActiveRobotTask = createTransferTask(pAligner, pFliper, primaryType, secondaryType);
+					m_pActiveRobotTask = createTransferTask(pAligner, pFliper, MaterialsType::G2, MaterialsType::G0);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
-
 				if (!rmd.armState[0]) {
-					m_pActiveRobotTask = createTransferTask(pAligner, pVacuumBake, primaryType, secondaryType);
+					m_pActiveRobotTask = createTransferTask(pAligner, pVacuumBake, MaterialsType::G1, MaterialsType::G0);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
-
-				// Aligner -> LoadPort
+				// 15) Aligner -> LoadPort(restore)
 				if (!rmd.armState[1]) {
 					m_pActiveRobotTask = createTransferTask_restore(pAligner, pLoadPorts);
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
+				// 16) LoadPort -> Aligner(受组数门限控制;统一 buddy/状态时序)
+				if (blockLoadFromLP) { unlock(); continue; }
 
-				// LoadPort -> Aligner
 				for (int s = 0; s < 4; s++) {
 					PortType pt = pLoadPorts[s]->getPortType();
 					if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
 						&& (pt == PortType::Loading || pt == PortType::Both)
 						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
-						m_pActiveRobotTask = createTransferTask(pLoadPorts[s], pAligner, primaryType, secondaryType, m_bJobMode);
+
+						m_pActiveRobotTask = createTransferTask(pLoadPorts[s], pAligner, primaryType, secondaryType, 1, m_bJobMode);
 						if (m_pActiveRobotTask != nullptr) {
-							CGlass* pGlass = (CGlass*)m_pActiveRobotTask->getContext();
+							auto* pGlass = static_cast<CGlass*>(m_pActiveRobotTask->getContext());
 							if (pGlass->getBuddy() != nullptr) {
-								delete m_pActiveRobotTask;
-								m_pActiveRobotTask = nullptr;
+								delete m_pActiveRobotTask; m_pActiveRobotTask = nullptr;
 								continue;
 							}
 
-							pEFEM->setContext(pGlass);
+							// 统一:queue -> start -> setContext -> move queue→inProcess -> onPanelStart
+							pGlass->queue();
 							pGlass->start();
+							pEFEM->setContext(pGlass);
+
 							bool bMoved = glassFromQueueToInPorcess(pGlass);
 							if (bMoved) {
-								LOGI("<Master>Glass(%s)从等待列队到工艺列队转移成功.",
-									pGlass->getID().c_str());
+								LOGI("<Master>Glass(%s)从等待列队到工艺列队转移成功.", pGlass->getID().c_str());
 							}
 							else {
-								LOGE("<Master>Glass(%s)从等待列队到工艺列队转移失败.",
-									pGlass->getID().c_str());
+								LOGE("<Master>Glass(%s)从等待列队到工艺列队转移失败.", pGlass->getID().c_str());
 							}
 
-							// 这里上报Panel Start事件
-							if (m_listener.onPanelStart != nullptr) {
-								m_listener.onPanelStart(this, pGlass);
-							}
-
+							if (m_listener.onPanelStart) m_listener.onPanelStart(this, pGlass);
 							goto BATCH_PORT_GET;
 						}
 					}
 				}
 
-			BATCH_PORT_GET:
+				BATCH_PORT_GET:
 				CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
-
 
 				unlock();
 				continue;
 			}
+
 
 			// 千传模式调度逻辑
 			else if (m_state == MASTERSTATE::RUNNING_CONTINUOUS_TRANSFER) {
@@ -1064,15 +1058,17 @@
 				}
 
 				// Measurement -> LoadPort
-				for (int s = 0; s < 4; s++) {
-					PortType pt = pLoadPorts[s]->getPortType();
+				for (int p = 0; p < 4; p++) {
+					if (p != m_nContinuousWorkingPort) continue;
+					PortType pt = pLoadPorts[p]->getPortType();
 					if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_Measurement)
-						&& !rmd.armState[0] && pLoadPorts[s]->isEnable()
+						&& !rmd.armState[0] && pLoadPorts[p]->isEnable()
 						&& (pt == PortType::Unloading || pt == PortType::Both)
-						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
+						&& pLoadPorts[p]->getPortStatus() == PORT_INUSE) {
 						for (int slot = 0; slot < SLOT_MAX; slot++) {
+							if (slot != m_nContinuousWorkingSlot) continue;
 							m_pActiveRobotTask = createTransferTask_continuous_transfer(pMeasurement,
-								0, pLoadPorts[s], slot);
+								0, pLoadPorts[p], slot);
 							if (m_pActiveRobotTask != nullptr) {
 								m_nContinuousTransferStep = CTStep_Measurement_LoadPort;
 								m_nContinuousTransferStep = CTStep_end;
@@ -1204,17 +1200,19 @@
 				}
 
 				// LoadPort -> Aligner
-				for (int s = 0; s < 4; s++) {
-					PortType pt = pLoadPorts[s]->getPortType();
+				for (int p = 0; p < 4; p++) {
+					PortType pt = pLoadPorts[p]->getPortType();
 					if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_end)
-						&& !rmd.armState[0] && pLoadPorts[s]->isEnable()
+						&& !rmd.armState[0] && pLoadPorts[p]->isEnable()
 						&& (pt == PortType::Loading || pt == PortType::Both)
-						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
+						&& pLoadPorts[p]->getPortStatus() == PORT_INUSE) {
 						for (int slot = 0; slot < SLOT_MAX; slot++) {
-							m_pActiveRobotTask = createTransferTask_continuous_transfer(pLoadPorts[s], 
+							m_pActiveRobotTask = createTransferTask_continuous_transfer(pLoadPorts[p], 
 								slot, pAligner, 0);
 							if (m_pActiveRobotTask != nullptr) {
 								m_nContinuousTransferStep = CTStep_LoadPort_Aligner;
+								m_nContinuousWorkingPort = p;
+								m_nContinuousWorkingSlot = slot;
 								LOGI("<ContinuousTransfer>千传测试,开始搬送任务(LoadPort -> Aligner)...");
 								pEFEM->setContext(m_pActiveRobotTask->getContext());
 								goto CT_PORT_GET;
@@ -1498,6 +1496,8 @@
 								if (m_listener.onCjEnd != nullptr) {
 									m_listener.onCjEnd(this, pJob);
 								}
+
+								completeControlJob();
 							}
 						}
 					}
@@ -1536,8 +1536,20 @@
 				unlock();
 			}
 		};
-		listener.onProcessStateChanged = [&](void* pEquipment, PROCESS_STATE state) -> void {
+		listener.onProcessStateChanged = [&](void* pEquipment, int slotNo, PROCESS_STATE state) -> void {
+			ASSERT(1 <= slotNo && slotNo <= 8);
+			int eqid = ((CEquipment*)pEquipment)->getID();
+			CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(slotNo);
 			LOGI("<Master>onProcessStateChanged<%d>", (int)state);
+			if (state == PROCESS_STATE::Processing) {
+				if (pGlass != nullptr) {
+					m_pCollector->batchStart(eqid,
+						pGlass->getID().c_str(), 10 * 60 * 1000ULL);
+				}
+			}
+			else if (state == PROCESS_STATE::Complete) {
+				m_pCollector->batchStop(eqid);
+			}
 		};
 		listener.onMapMismatch = [&](void* pEquipment, short scanMap, short downMap) {
 			LOGE("<Master-%s>Port InUse, map(%d!=%d)不一致,请检查。",
@@ -1551,17 +1563,58 @@
 				for (auto pj : pjs) {
 					auto carrier = pj->getCarrier(pPort->getCassetteId());
 					if (carrier != nullptr) {
+						carrier->contexts.clear();
 						for (auto slot : carrier->slots) {
 							CGlass* pGlass = pPort->getGlassFromSlot(slot);
 							carrier->contexts.push_back((void*)pGlass);
 							if (pGlass != nullptr) {
 								pGlass->setProcessJob(pj);
+
+								PJWarp& jpWarp = pj->getPjWarp();
+								int nRecipeID = RecipeManager::getInstance().getIdByPPID(pj->recipeSpec());
+								RecipeInfo stRecipeInfo = RecipeManager::getInstance().getRecipeByPPID(pj->recipeSpec());
+								std::vector<DeviceRecipe> vecRecipeInfo = stRecipeInfo.vecDeviceList;
+
+								pGlass->setScheduledForProcessing(jpWarp.checkSlot[slot-1]);
+								pGlass->setType(static_cast<SERVO::MaterialsType>(jpWarp.material[slot-1]));
+
+								SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
+								if (pJobDataS != nullptr) {
+									SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
+									pJobDataS->setLotId(pj->getLotId().c_str());
+									pJobDataS->setProductId(pj->getProductId().c_str());
+									pJobDataS->setOperationId(pj->getOperationId().c_str());
+									pJobDataS->setMaterialsType(jpWarp.material[slot - 1]);
+									pJobDataS->setMasterRecipe(nRecipeID);
+									for (const auto& info : vecRecipeInfo) {
+										const std::string& name = info.strDeviceName;
+										short nRecipeID = (short)info.nRecipeID;
+
+										if (name == EQ_NAME_EFEM) {
+											pJobDataS->setDeviceRecipeId(0, nRecipeID);
+										}
+										else if (name == EQ_NAME_BONDER1) {
+											pJobDataS->setDeviceRecipeId(1, nRecipeID);
+										}
+										else if (name == EQ_NAME_BONDER2) {
+											pJobDataS->setDeviceRecipeId(2, nRecipeID);
+										}
+										else if (name == EQ_NAME_BAKE_COOLING) {
+											pJobDataS->setDeviceRecipeId(3, nRecipeID);
+										}
+										else if (name == EQ_NAME_VACUUMBAKE) {
+											pJobDataS->setDeviceRecipeId(4, nRecipeID);
+										}
+										else if (name == EQ_NAME_MEASUREMENT) {
+											pJobDataS->setDeviceRecipeId(5, nRecipeID);
+										}
+									}
+
+								}
 							}
 						}
 					}
 				}
-
-
 			}
 			
 			if (m_listener.onLoadPortStatusChanged != nullptr) {
@@ -1574,6 +1627,81 @@
 			std::vector<CParam> params;
 			((CEquipment*)pEquipment)->parsingSVData((const char*)rawData.data(), rawData.size(), params);
 		
+
+			// 以下加入到曲线数据中
+
+
+			const int64_t ts = now_ms_epoch();
+			int eqid = ((CEquipment*)pEquipment)->getID();
+			if (eqid == EQ_ID_Bonder1 || eqid == EQ_ID_Bonder2) {
+				// 定义 Bonder 的特定映射
+				std::vector<std::pair<int, int>> bonderMapping = {
+					{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7},
+					{8, 8}, {9, 9}, {10, 10}, {11, 11}, {12, 12}, {13, 13}, {14, 14}, {15, 15}, {16, 16}
+				};
+
+				CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
+				auto& dataTypes = CServoUtilsTool::getEqDataTypes();
+				auto& bonderTypes = dataTypes[eqid];
+				for (const auto& mapping : bonderMapping) {
+					int paramIndex = mapping.first;
+					int channel = mapping.second;
+
+					if (paramIndex < params.size() && channel - 1 < bonderTypes.size()) {
+						if(m_pCollector != nullptr)
+							m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
+						if(pGlass != nullptr)
+							pGlass->addSVData(eqid, bonderTypes[channel], ts, params.at(paramIndex).getDoubleValue());
+					}
+				}
+			}
+			else if (eqid == EQ_ID_VACUUMBAKE) {
+				// 定义 VACUUMBAKE 的特定映射
+				std::vector<std::pair<int, int>> vacuumMapping = {
+					{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7},
+					{10, 8}, {11, 9}, {12, 10}, {13, 11}, {14, 12}, {15, 13}, {16, 14}
+				};
+
+				CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
+				auto& dataTypes = CServoUtilsTool::getEqDataTypes();
+				auto& vacuumbakeTypes = dataTypes[eqid];
+				for (const auto& mapping : vacuumMapping) {
+					int paramIndex = mapping.first;
+					int channel = mapping.second;
+
+					if (paramIndex < params.size() && channel - 1 < vacuumbakeTypes.size()) {
+						if (m_pCollector != nullptr)
+							m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
+						if (pGlass != nullptr)
+							pGlass->addSVData(eqid, vacuumbakeTypes[channel], ts, params.at(paramIndex).getDoubleValue());
+					}
+				}
+			}
+			else if (eqid == EQ_ID_BAKE_COOLING) {
+				// 定义 BAKE_COOLING 的特定映射
+				std::vector<std::pair<int, int>> coolingMapping = {
+					{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6},
+					{11, 7}, {12, 8}, {13, 9}, {14, 10}, {15, 11}, {16, 12}
+				};
+
+				CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
+				auto& dataTypes = CServoUtilsTool::getEqDataTypes();
+				auto& coolingTypes = dataTypes[eqid];
+				for (const auto& mapping : coolingMapping) {
+					int paramIndex = mapping.first;
+					int channel = mapping.second;
+
+					if (paramIndex < params.size() && channel - 1 < coolingTypes.size()) {
+						if (m_pCollector != nullptr)
+							m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
+						if (pGlass != nullptr)
+							pGlass->addSVData(eqid, coolingTypes[channel], ts, params.at(paramIndex).getDoubleValue());
+					}
+				}
+			}
+
+
+			// 以下是输出测试
 			std::string strOut;
 			char szBuffer[256];
 			for (auto p : params) {
@@ -1847,6 +1975,7 @@
 
 
 		// 模拟测试
+		/*
 		static int aaa = 0;
 		aaa++;
 		if (aaa % 30 == 0) {
@@ -1900,7 +2029,7 @@
 				}
 			}
 		}
-		
+		*/
 	}
 
 	void CMaster::connectEquipments()
@@ -2285,11 +2414,19 @@
 		return 0;
 	}
 
-	void CMaster::setPortType(unsigned int index, BOOL enable, int type, int mode,
-		int cassetteType, int transferMode, BOOL autoChangeEnable)
+	void CMaster::setPortType(unsigned int index, int type)
 	{
 		ASSERT(index < 4);
 		int eqid[] = { EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4};
+		CLoadPort* pPort = (CLoadPort*)getEquipment(eqid[index]);
+		pPort->localSetPortType((SERVO::PortType)type);
+	}
+
+	void CMaster::setPortTypeEx(unsigned int index, BOOL enable, int type, int mode,
+		int cassetteType, int transferMode, BOOL autoChangeEnable)
+	{
+		ASSERT(index < 4);
+		int eqid[] = { EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4 };
 		CLoadPort* pPort = (CLoadPort*)getEquipment(eqid[index]);
 		pPort->localEanblePort(enable);
 		pPort->localSetPortType((SERVO::PortType)type);
@@ -2403,8 +2540,31 @@
 				temp.push_back(p);
 			}
 		}
-
 		m_processJobs = temp;
+
+
+		// 更新context
+		std::vector<uint8_t> newSlots;
+		std::vector<void*> newContexts;
+		for (auto pj : m_processJobs) {
+			for (auto& c : pj->carriers()) {
+				auto pPort = getPortWithCarrierId(c.carrierId);
+				if (pPort == nullptr) continue;
+
+				for (auto s : c.slots) {
+					auto pGlass = pPort->getGlassFromSlot(s);
+					if (pGlass == nullptr) continue;
+
+					newSlots.push_back(s);
+					newContexts.push_back(pGlass);
+				}
+
+				pj->setCarrierSlotsAndContexts(c.carrierId, newSlots, newContexts);
+			}
+		}
+
+
+
 		this->saveState();
 
 		return (int)m_processJobs.size();
@@ -2547,13 +2707,9 @@
 		return true;
 	}
 
-	bool CMaster::loadState(const std::string& path)
+	bool CMaster::loadState()
 	{
-		// 保存文件路径
-		m_strStatePath = path;
-
-
-		std::ifstream ifs(path, std::ios::binary);
+		std::ifstream ifs(m_strStatePath, std::ios::binary);
 		if (!ifs) return false;
 
 		// 文件头
@@ -2606,10 +2762,33 @@
 		m_pControlJob->setPJs(tempPjs);
 
 
+		// 更新contexts
+		auto pjs = m_pControlJob->getPjs();
+		for (auto pj : pjs) {
+			for (auto& c : pj->carriers()) {
+				auto p = getPortWithCarrierId(c.carrierId);
+				if (p == nullptr) continue;
+
+				std::vector<void*> contexts;
+				for (auto s : c.slots) {
+					auto g = getGlass(p->getIndex(), s - 1);
+					if (g == nullptr) continue;
+					contexts.push_back(g);
+				}
+				pj->setCarrierContexts(c.carrierId, contexts);
+			}
+		}
+
+
 		// 如果版本升级,可在这里判断 version 来加载新字段
 
 
 		return true;
+	}
+
+	void CMaster::setStateFile(const std::string& path)
+	{
+		m_strStatePath = path;
 	}
 
 	CProcessJob* CMaster::acquireNextProcessJob()
@@ -2648,6 +2827,7 @@
 		int nCount = 0;
 		for (auto* pj : m_inProcesJobs) {
 			// 遍历 PJ 的 carriers 和 slots
+			if (pj->carriers().empty()) continue;
 			for (auto& cs : pj->carriers()) {
 				for (auto ctx : cs.contexts) {
 					CGlass* pGlass = (CGlass*)ctx;
@@ -2752,6 +2932,94 @@
 		return nullptr;
 	}
 
+
+	bool CMaster::completeControlJob()
+	{
+		if (m_pControlJob == nullptr) {
+			return false;
+		}
+		for (auto item : m_processJobs) {
+			if (item->state() != PJState::Completed) return false;
+		}
+		if (m_pControlJob->state() != CJState::Completed)
+			return false;
+
+
+
+		// 释放Job相关
+		for (auto item : m_processJobs) {
+			delete item;
+		}
+		m_processJobs.clear();
+		if (m_pControlJob != nullptr) {
+			delete m_pControlJob;
+			m_pControlJob = nullptr;
+		}
+
+		// 注意要释放引用
+		m_inProcesJobs.clear();
+		m_completeProcessJobs.clear();
+		m_queueGlasses.clear();
+		m_inProcesGlasses.clear();
+		m_completeGlasses.clear();
+
+
+		saveState();
+
+		return true;
+	}
+
+	bool CMaster::forceCompleteControlJob(std::string description)
+	{
+		if (m_pControlJob == nullptr || m_state != SERVO::MASTERSTATE::READY) {
+			return false;
+		}
+		for (auto item : m_processJobs) {
+			item->abort(description);
+		}
+		m_pControlJob->abort(description);
+
+
+		// 释放Job相关
+		for (auto item : m_processJobs) {
+			delete item;
+		}
+		m_processJobs.clear();
+		if (m_pControlJob != nullptr) {
+			delete m_pControlJob;
+			m_pControlJob = nullptr;
+		}
+
+		// 注意要释放引用
+		m_inProcesJobs.clear();
+		m_completeProcessJobs.clear();
+		m_queueGlasses.clear();
+		m_inProcesGlasses.clear();
+		m_completeGlasses.clear();
+
+
+		saveState();
+
+		return true;
+	}
+
+	bool CMaster::canCreateControlJob()
+	{
+		return m_pControlJob == nullptr;
+	}
+
+	bool CMaster::canCompleteControlJob()
+	{
+		return m_pControlJob != nullptr && m_state == SERVO::MASTERSTATE::READY;
+	}
+
+	bool CMaster::canDeleteControlJob()
+	{
+		return m_pControlJob != nullptr 
+			&& m_pControlJob->state() == CJState::NoState
+			&& m_state == SERVO::MASTERSTATE::READY;
+	}
+
 	int CMaster::getWipGlasses(std::vector<CGlass*>& glasses)
 	{
 		for (auto eq : m_listEquipment) {
@@ -2826,4 +3094,71 @@
 
 		return true;
 	}
+
+	CGlass* CMaster::getGlass(int scrPort, int scrSlot)
+	{
+		for (auto eq : m_listEquipment) {
+			std::vector<CGlass*> glasses;
+			eq->getAllGlass(glasses);
+			for (auto g : glasses) {
+				int p, s;
+				g->getOrginPort(p, s);
+				if (p == scrPort && s == scrSlot) {
+					return g;
+				}
+			}
+		}
+
+		return nullptr;
+	}
+
+	void CMaster::CreateDAQBridgeServer()
+	{
+		auto connectionStatusCallback = [&](int code, const std::string& status) {
+			LOGI("<DAQBridge>status:", status.c_str());
+		};
+		auto rawDataCallback = [](const std::vector<uint8_t>& bytes) {
+
+		};
+
+		// 事件:有人连入/断开就上日志
+		auto clieintEventCallback = [](const std::string& ip, uint16_t port, bool connected) {
+			LOGI("<DAQBridge>[Client %s] %s:%u", connected ? _T("JOIN") : _T("LEAVE"), ip.c_str(), port);
+		};
+
+		if (m_pCollector == nullptr) {
+			m_pCollector = new Collector();
+			m_pCollector->setConnectionStatusCallback(connectionStatusCallback);
+			m_pCollector->setRawDataCallback(rawDataCallback);
+			m_pCollector->setClientEventCallback(clieintEventCallback);
+			m_pCollector->createServer(8081);
+			m_pCollector->startLoop(10);
+
+			// 1) 注册机台(推荐:先注册 id + 机器名称)
+			RetentionPolicy defP; defP.mode = RetainMode::ByCount; defP.maxSamples = 200;
+			m_pCollector->registryAddMachine(EQ_ID_Bonder1, "Bonder1", defP);
+			m_pCollector->registryAddMachine(EQ_ID_Bonder2, "Bonder2", defP);
+			m_pCollector->registryAddMachine(EQ_ID_VACUUMBAKE, "前烘烤", defP);
+			m_pCollector->registryAddMachine(EQ_ID_BAKE_COOLING, "烘烤冷却", defP);
+
+
+			// 2) 为通道设置“曲线名称”
+			auto& dataTypes = CServoUtilsTool::getEqDataTypes();
+			auto& bonderTypes = dataTypes[EQ_ID_Bonder1];
+			for (size_t i = 0; i < bonderTypes.size(); ++i) {
+				m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, i + 1, bonderTypes[i].c_str());
+				m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, i + 1, bonderTypes[i].c_str());
+			}
+
+			auto& vacuumbakeTypes = dataTypes[EQ_ID_VACUUMBAKE];
+			for (size_t i = 0; i < vacuumbakeTypes.size(); ++i) {
+				m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, i + 1, vacuumbakeTypes[i].c_str());
+			}
+
+			auto& coolingTypes = dataTypes[EQ_ID_BAKE_COOLING];
+			for (size_t i = 0; i < coolingTypes.size(); ++i) {
+				m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, i + 1, coolingTypes[i].c_str());
+			}
+		}
+	}
 }
diff --git a/SourceCode/Bond/Servo/CMaster.h b/SourceCode/Bond/Servo/CMaster.h
index ebca19b..e2844bd 100644
--- a/SourceCode/Bond/Servo/CMaster.h
+++ b/SourceCode/Bond/Servo/CMaster.h
@@ -1,4 +1,4 @@
-#pragma once
+锘�#pragma once
 #include <list>
 #include "CEquipment.h"
 #include "CEFEM.h"
@@ -15,6 +15,7 @@
 #include "CRobotTask.h"
 #include "ProcessJob.h"
 #include "CControlJob.h"
+#include "../DAQBridge/core/Collector.h"
 
 
 #define CTStep_Unknow                   0
@@ -107,8 +108,9 @@
         int abortCurrentTask();
         int restoreCurrentTask();
         int resendCurrentTask();
-        void setPortType(unsigned int index, BOOL enable, int type, int mode,
+        void setPortTypeEx(unsigned int index, BOOL enable, int type, int mode,
             int cassetteType, int transferMode, BOOL autoChangeEnable);
+        void setPortType(unsigned int index, int type);
         void setPortCassetteType(unsigned int index, SERVO::CassetteType type);
         void setPortEnable(unsigned int index, BOOL bEnable);
         void setCompareMapsBeforeProceeding(BOOL bCompare);
@@ -128,13 +130,15 @@
         CControlJob* getControlJob();
         CLoadPort* getPortWithCarrierId(const std::string& carrierId) const;
         bool saveState() const;
-        bool loadState(const std::string& path);
+        bool loadState();
+        void setStateFile(const std::string& path);
         int getWipGlasses(std::vector<CGlass*>& glasses);
         void test();
         bool moveGlassToBuf(int eqid, int slotNo);
         bool moveGlassToSlot(int eqid, int slotNo);
         int getPortCassetteSnSeed(int port);
         void setPortCassetteSnSeed(int port, int seed);
+        CGlass* getGlass(int scrPort, int scrSlot);
 
     private:
         inline void lock() { EnterCriticalSection(&m_criticalSection); }
@@ -167,7 +171,7 @@
             CEquipment* pTarEq, int nTarSlot, int armNo = 1);
 
     public:
-        // —— IResourceView 覆写 ——(注意 const)
+        // 鈥斺�� IResourceView 瑕嗗啓 鈥斺�旓紙娉ㄦ剰 const锛�
         bool isProcessJobsEmpty() const override;
         bool recipeExists(const std::string& ppid) const override;
         bool carrierPresent(const std::string& carrierId) const override;
@@ -179,7 +183,7 @@
         std::string& getLastErrorText();
 
     public:
-        // 新增函数
+        // 鏂板鍑芥暟
         CProcessJob* acquireNextProcessJob();
         CGlass* acquireNextGlass();
         int acquireGlassToQueue();
@@ -190,7 +194,14 @@
         bool checkAndUpdatePjComplete(CProcessJob* pJob);
         bool checkAndUpdateCjComplete(CControlJob* pJob);
         CProcessJob* getGlassProcessJob(CGlass* pGlass);
-
+        bool completeControlJob();
+        bool forceCompleteControlJob(std::string description);
+        bool canCreateControlJob();
+        bool canCompleteControlJob();
+        bool canDeleteControlJob();
+        
+        // DAQ Bridge閻╃鍙�
+        Collector* getCollector() const { return m_pCollector; }
 
     private:
         CRITICAL_SECTION m_criticalSection;
@@ -203,40 +214,42 @@
         bool m_bBatch;
 
     private:
-        /* 监控比特位的线程*/
+        /* 鐩戞帶姣旂壒浣嶇殑绾跨▼*/
         HANDLE m_hEventReadBitsThreadExit[2];
         HANDLE m_hReadBitsThreadHandle;
         unsigned m_nReadBitsThreadAddr;
 
-        // 调度线程
+        // 璋冨害绾跨▼
         HANDLE m_hDispatchEvent;
         HANDLE m_hEventDispatchThreadExit[2];
         HANDLE m_hDispatchThreadHandle;
         unsigned m_nDispatchThreadAddr;
 
-        // 启动时间,运行时间,状态
+        // 鍚姩鏃堕棿锛岃繍琛屾椂闂达紝鐘舵��
         ULONGLONG m_ullStartTime;
         ULONGLONG m_ullRunTime;
         MASTERSTATE m_state;
 
-        // 当前任务和已完成任务列表
+        // 褰撳墠浠诲姟鍜屽凡瀹屾垚浠诲姟鍒楄〃
         CRobotTask* m_pActiveRobotTask;
         std::list< CRobotTask* > m_listTask;
 
-        // 错误代码
+        // 閿欒浠g爜
         int m_nLastError;
         std::string m_strLastError;
 
-        // 在开始工艺前是否先需要先比较map
+        // 鍦ㄥ紑濮嬪伐鑹哄墠鏄惁鍏堥渶瑕佸厛姣旇緝map
         BOOL m_isCompareMapsBeforeProceeding;
         BOOL m_bJobMode;
 
 
-        // 千传圈数计数
+        // 鍗冧紶鍦堟暟璁℃暟
         int m_nContinuousTransferCount;
         int m_nContinuousTransferStep;
+        int m_nContinuousWorkingPort;
+        int m_nContinuousWorkingSlot;
 
-        // 新增已经开始处理的ProcessJob列表
+        // 鏂板宸茬粡寮�濮嬪鐞嗙殑ProcessJob鍒楄〃
         std::vector<CProcessJob*> m_inProcesJobs;
         std::vector<CProcessJob*> m_completeProcessJobs;
         std::vector<CGlass*> m_queueGlasses;
@@ -252,6 +265,10 @@
 
         int m_nTestFlag;
         std::list<CGlass*> m_bufGlass;
+
+    private:
+        Collector* m_pCollector = nullptr;
+        void CreateDAQBridgeServer();
     };
 }
 
diff --git a/SourceCode/Bond/Servo/CMeasurement.cpp b/SourceCode/Bond/Servo/CMeasurement.cpp
index d3f1533..67f9c9f 100644
--- a/SourceCode/Bond/Servo/CMeasurement.cpp
+++ b/SourceCode/Bond/Servo/CMeasurement.cpp
@@ -455,22 +455,37 @@
 		params.push_back(CParam("检测速度", "", this->getName().c_str(), v * 0.001));
 		i += 4;
 
+		return (int)params.size();
+	}
+
+	int CMeasurement::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
+	{
+		ASSERT(pszData);
+		if (size < 250) return 0;
+		int i = 0, v;
+
+
+		// 1.检测功能启用/禁用
+		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+		params.push_back(CParam("检测功能启用/禁用", "", this->getName().c_str(), v));
+		i += 2;
+
+		// 2.检测速度
+		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
+		params.push_back(CParam("检测速度", "", this->getName().c_str(), v * 0.001));
+		i += 4;
+
 		// 3.检测1数据
 		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
 		params.push_back(CParam("检测1数据", "", this->getName().c_str(), v * 0.001));
 		i += 4;
-		
+
 		// 4.检测2数据
 		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
 		params.push_back(CParam("检测2数据", "", this->getName().c_str(), v * 0.001));
 		i += 4;
 
 		return (int)params.size();
-	}
-
-	int CMeasurement::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
-	{
-		return parsingParams(pszData, size, params);
 	}
 
 	int CMeasurement::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
diff --git a/SourceCode/Bond/Servo/CPageCollectionEvent.cpp b/SourceCode/Bond/Servo/CPageCollectionEvent.cpp
index a7108fb..b7d3322 100644
--- a/SourceCode/Bond/Servo/CPageCollectionEvent.cpp
+++ b/SourceCode/Bond/Servo/CPageCollectionEvent.cpp
@@ -106,7 +106,6 @@
 	CHMPropertyPage::OnSize(nType, cx, cy);
 	if (GetDlgItem(IDC_LIST1) == nullptr) return;
 
-	CWnd* pItem;
 	CRect rcClient, rcItem;
 	GetClientRect(&rcClient);
 	m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
diff --git a/SourceCode/Bond/Servo/CPageGlassList.cpp b/SourceCode/Bond/Servo/CPageGlassList.cpp
index 4291a7e..cbe79ed 100644
--- a/SourceCode/Bond/Servo/CPageGlassList.cpp
+++ b/SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -100,6 +100,9 @@
 // ====== 寮�鍏筹細1=鍚敤鍋囨暟鎹紙鍙浛鎹� DB 鏌ヨ锛夛紱0=鐢ㄧ湡瀹� DB ======
 #define USE_FAKE_DB_DEMO 0
 
+// ====== 寮�鍏筹細1=鍚敤妯℃嫙浼犳劅鍣ㄦ暟鎹敓鎴愶紱0=浣跨敤鐪熷疄鏁版嵁 ======
+#define USE_MOCK_SENSOR_DATA 0
+
 #if USE_FAKE_DB_DEMO
 #include <ctime>
 #include <atlconv.h>   // CStringA
@@ -372,6 +375,55 @@
     return true;
 }
 
+// 杈呭姪鍑芥暟锛氬皢 ANSI CString 鍐欏叆鏂囦欢涓� UTF-8 缂栫爜
+bool CPageGlassList::WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath)
+{
+    CFile file;
+    if (!file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
+        return false;
+    }
+
+    // 鍐欏叆 UTF-8 BOM
+    const unsigned char bom[] = { 0xEF, 0xBB, 0xBF };
+    file.Write(bom, 3);
+
+    // 灏� ANSI 杞崲涓� Unicode
+    int unicodeLength = MultiByteToWideChar(CP_ACP, 0,
+        ansiContent, ansiContent.GetLength(),
+        NULL, 0);
+
+    if (unicodeLength <= 0) {
+        file.Close();
+        return false;
+    }
+
+    wchar_t* unicodeBuffer = new wchar_t[unicodeLength + 1];
+    MultiByteToWideChar(CP_ACP, 0,
+        ansiContent, ansiContent.GetLength(),
+        unicodeBuffer, unicodeLength);
+    unicodeBuffer[unicodeLength] = 0;
+
+    // 灏� Unicode 杞崲涓� UTF-8
+    int utf8Length = WideCharToMultiByte(CP_UTF8, 0,
+        unicodeBuffer, unicodeLength,
+        NULL, 0, NULL, NULL);
+
+    bool success = false;
+    if (utf8Length > 0) {
+        char* utf8Buffer = new char[utf8Length];
+        WideCharToMultiByte(CP_UTF8, 0,
+            unicodeBuffer, unicodeLength,
+            utf8Buffer, utf8Length, NULL, NULL);
+
+        file.Write(utf8Buffer, utf8Length);
+        delete[] utf8Buffer;
+        success = true;
+    }
+
+    delete[] unicodeBuffer;
+    file.Close();
+    return success;
+}
 
 // CPageGlassList 瀵硅瘽妗�
 
@@ -426,6 +478,7 @@
     ON_BN_CLICKED(IDC_BUTTON_PREV_PAGE, &CPageGlassList::OnBnClickedButtonPrevPage)
     ON_BN_CLICKED(IDC_BUTTON_NEXT_PAGE, &CPageGlassList::OnBnClickedButtonNextPage)
     ON_NOTIFY(ELCN_SHOWFULLTEXT, IDC_LIST_ALARM, &CPageGlassList::OnShowFullText)
+    ON_BN_CLICKED(IDC_BUTTON_EXPORT_ROW, &CPageGlassList::OnBnClickedButtonExportRow)
 END_MESSAGE_MAP()
 
 // ===== 绉佹湁灏忓伐鍏� =====
@@ -661,106 +714,163 @@
     auto pageFull = db.queryPaged(m_filters, rawLimit, rawOffset);
 #endif
 
-    // 濡傛灉澶氬嚭涓�鏉★紝鐪嬬湅瀹冩槸鍚︽槸鈥滄湰椤垫渶鍚庝竴鏉♀�濈殑 buddy
-    std::optional<decltype(pageFull.items)::value_type> lookahead; // 棰勮璁板綍锛堣嫢涓庢渶鍚庝竴鏉¢厤瀵癸級
+#if !USE_FAKE_DB_DEMO
+    // 鈥斺�� 涓夊厓閿伐鍏凤細<classId>|C<cassette>|J<job> 鈥斺�� //
+// 鈥斺�� 涓夊厓閿伐鍏凤細<classId>|C<cassette>|J<job> 鈥斺�� //
+    auto makeKey = [](const std::string& cls, int csn, int jsn) -> std::string {
+        std::string k;
+        k.reserve(cls.size() + 32);
+        k.append(cls);
+        k.push_back('|'); k.push_back('C');
+        k.append(std::to_string(csn));
+        k.push_back('|'); k.push_back('J');
+        k.append(std::to_string(jsn));
+        return k;
+    };
+
+    // 鈽呪槄鈽� 杩欓噷鏄叧閿慨澶嶏細鎺ユ敹鈥渃onst Row&鈥濓紝涓嶈闈� const 寮曠敤
+    using RowT = std::decay<decltype(pageFull.items.front())>::type;
+    auto makeKeyR = [&](const RowT& r) -> std::string {
+        return makeKey(r.classId, r.cassetteSeqNo, r.jobSeqNo);
+    };
+
+    // 涓嶅尯鍒嗗ぇ灏忓啓 classId 鐩哥瓑
     auto iEquals = [](const std::string& a, const std::string& b) {
 #ifdef _WIN32
         return _stricmp(a.c_str(), b.c_str()) == 0;
 #else
         return strcasecmp(a.c_str(), b.c_str()) == 0;
 #endif
-    };
+};
 
+
+    // 鈥斺�� lookahead 棰勮锛氳嫢瓒呭嚭 1 鏉★紝灏濊瘯鎶娾�滄渶鍚庝竴鏉♀�濅笌鈥滈璇烩�濆垽涓轰竴瀵癸紙涓ユ牸浼樺厛锛夆�斺��
+    std::optional<decltype(pageFull.items)::value_type> lookahead;
     if (pageFull.items.size() == rawLimit) {
         const auto& last = pageFull.items[PAGE_SIZE - 1];
         const auto& extra = pageFull.items[PAGE_SIZE];
 
-        bool pair =
+        bool strictPair =
+            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)
+                && last.cassetteSeqNo == extra.cassetteSeqNo
+                && last.jobSeqNo == extra.jobSeqNo)
+            || (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId)
+                && extra.cassetteSeqNo == last.cassetteSeqNo
+                && extra.jobSeqNo == last.jobSeqNo);
+
+        bool loosePair =
             (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) ||
             (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId));
 
-        if (pair) {
-            lookahead = extra;           // 鎶婇璇讳繚瀛樹笅鏉ワ紝绋嶅悗琛ユ垚瀛愯
+        if (strictPair || loosePair) {
+            lookahead = extra;
         }
-        // 鏃犺鏄惁閰嶅锛屽垪琛ㄩ兘缂╁洖 PAGE_SIZE 鏉★紙棰勮涓嶇畻鍏ユ湰椤垫暟鎹泦锛�
+        // 棰勮涓嶇畻鍏ユ湰椤�
         pageFull.items.pop_back();
     }
 
     // 涔嬪悗姝e父鎸� page 鏋勫缓
-    auto& page = pageFull; // 涓轰簡澶嶇敤浣犲師鏈夊彉閲忓悕
+    auto& pageRef = pageFull;
 
-    // 寤虹储寮曪細classId -> index
-    std::unordered_map<std::string, size_t> idxById;
-    idxById.reserve(page.items.size());
-    for (size_t i = 0; i < page.items.size(); ++i) {
-        idxById[page.items[i].classId] = i;
+    // 鈥斺�� 寤轰袱涓储寮� 鈥斺�� //
+    // A) byTriple: 涓夊厓閿� -> index锛堝敮涓�/宸叉秷璐逛緷鎹級
+    // B) byClass : classId -> indices锛坆uddy 鍊欓�夋睜锛屽厑璁稿涓級
+    std::unordered_map<std::string, size_t> byTriple;
+    std::unordered_map<std::string, std::vector<size_t>> byClass;
+    byTriple.reserve(pageRef.items.size());
+    byClass.reserve(pageRef.items.size());
+
+    for (size_t i = 0; i < pageRef.items.size(); ++i) {
+        const auto& r = pageRef.items[i];
+        byTriple[makeKeyR(r)] = i;
+        byClass[r.classId].push_back(i);
     }
 
-    // 宸叉秷璐癸紙宸叉彃鍏ヤ负鐖舵垨瀛愶級
+    // 鈥斺�� 宸叉秷璐归泦鍚堬紙鐢ㄤ笁鍏冮敭锛夆�斺��
     std::unordered_set<std::string> consumed;
+    consumed.reserve(pageRef.items.size());
+
     int zebra = 0;
     auto zebraBk = [&](int z) -> COLORREF {
         return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
     };
 
-    // -------- Phase 1: 鍏堝鐞嗏�滄湁 buddyId 鐨勮褰曗�濓紙鑳介厤灏遍厤锛涘崟鍚戜篃閰嶏級 ----------
-    for (size_t i = 0; i < page.items.size(); ++i) {
-        const auto& r = page.items[i];
-        // CopyUtf8ToClipboard(r.pretty);
-        
-        if (consumed.count(r.classId)) continue;
+    // -------- Phase 1: 鍏堝鐞嗏�滄湁 buddyId 鐨勮褰曗�� ----------
+    for (size_t i = 0; i < pageRef.items.size(); ++i) {
+        const auto& r = pageRef.items[i];
+        if (consumed.count(makeKeyR(r))) continue;
         if (r.buddyId.empty()) continue;
 
-        COLORREF bk = zebraBk(zebra);
+        // 鍦ㄥ悓椤甸噷涓� r 鎵� buddy 鍊欓��
+        size_t buddyIdx = (size_t)-1;
+        auto itVec = byClass.find(r.buddyId);
+        if (itVec != byClass.end()) {
+            const auto& vec = itVec->second;
 
-        auto it = idxById.find(r.buddyId);
-        if (it != idxById.end()) {
-            const auto& br = page.items[it->second];
-            if (!consumed.count(br.classId)) {
-                // 鈥斺�� 浠モ�滄湁 buddyId 鐨勮繖鏉� r鈥濅负鐖讹紝buddy 浣滀负瀛愶紙鍗曞悜涔熻兘閰嶏級鈥斺��
-                std::vector<CString> pcols(colCount);
-                pcols[1] = std::to_string(r.id).c_str();
-                pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
-                pcols[3] = std::to_string(r.jobSeqNo).c_str();
-                pcols[4] = r.classId.c_str();
-                pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
-                pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
-                pcols[7] = r.tStart.c_str();
-                pcols[8] = r.tEnd.c_str();
-                pcols[9] = r.buddyId.c_str();
-                pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
-                pcols[11] = r.path.c_str();
-                pcols[12] = r.params.c_str();
-
-                auto* nParent = m_listCtrl.InsertRoot(pcols);
-                MaybeRestoreExpandByKey(nParent, expandedKeys);
-                m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
-
-                std::vector<CString> ccols(colCount);
-                ccols[1] = std::to_string(br.id).c_str();
-                ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
-                ccols[3] = std::to_string(br.jobSeqNo).c_str();
-                ccols[4] = br.classId.c_str();
-                ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
-                ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
-                ccols[7] = br.tStart.c_str();
-                ccols[8] = br.tEnd.c_str();
-                ccols[9] = br.buddyId.c_str();
-                ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
-                ccols[11] = br.path.c_str();
-                ccols[12] = br.params.c_str();
-
-                auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
-                m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
-
-                consumed.insert(r.classId);
-                consumed.insert(br.classId);
-                ++zebra;
-                continue;
+            // 1) 涓ユ牸鍖归厤锛欳assette/Job 涓�鑷�
+            for (size_t j : vec) {
+                const auto& br = pageRef.items[j];
+                if (br.cassetteSeqNo == r.cassetteSeqNo && br.jobSeqNo == r.jobSeqNo) {
+                    if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; }
+                }
+            }
+            // 2) 瀹芥澗鍖归厤锛氬悓 classId 鏈秷璐圭殑浠绘剰涓�鏉�
+            if (buddyIdx == (size_t)-1) {
+                for (size_t j : vec) {
+                    const auto& br = pageRef.items[j];
+                    if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; }
+                }
             }
         }
 
-        // 鍚岄〉娌℃壘鍒� buddy锛堟垨宸茶娑堣垂锛夆啋 鎻掑崰浣嶅瓙琛�
+        COLORREF bk = zebraBk(zebra);
+
+        if (buddyIdx != (size_t)-1) {
+            const auto& br = pageRef.items[buddyIdx];
+
+            // 鐖讹細r锛堟湁 buddyId锛夛紝瀛愶細br
+            std::vector<CString> pcols(colCount);
+            pcols[1] = std::to_string(r.id).c_str();
+            pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
+            pcols[3] = std::to_string(r.jobSeqNo).c_str();
+            pcols[4] = r.classId.c_str();
+            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
+            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
+            pcols[7] = r.tStart.c_str();
+            pcols[8] = r.tEnd.c_str();
+            pcols[9] = r.buddyId.c_str();
+            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
+            pcols[11] = r.path.c_str();
+            pcols[12] = r.params.c_str();
+
+            auto* nParent = m_listCtrl.InsertRoot(pcols);
+            MaybeRestoreExpandByKey(nParent, expandedKeys);
+            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
+
+            std::vector<CString> ccols(colCount);
+            ccols[1] = std::to_string(br.id).c_str();
+            ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
+            ccols[3] = std::to_string(br.jobSeqNo).c_str();
+            ccols[4] = br.classId.c_str();
+            ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
+            ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
+            ccols[7] = br.tStart.c_str();
+            ccols[8] = br.tEnd.c_str();
+            ccols[9] = br.buddyId.c_str();
+            ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
+            ccols[11] = br.path.c_str();
+            ccols[12] = br.params.c_str();
+
+            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
+            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
+
+            consumed.insert(makeKeyR(r));
+            consumed.insert(makeKeyR(br));
+            ++zebra;
+            continue;
+        }
+
+        // 娌℃壘鍒� buddy 鈫� 鎻掑崰浣嶅瓙琛岋紙鍙啓 ClassID锛�
         std::vector<CString> pcols(colCount);
         pcols[1] = std::to_string(r.id).c_str();
         pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
@@ -777,24 +887,162 @@
 
         auto* nParent = m_listCtrl.InsertRoot(pcols);
         MaybeRestoreExpandByKey(nParent, expandedKeys);
-        m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
+        m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), zebraBk(zebra));
 
-        std::vector<CString> ccols(colCount); // 鍗犱綅鍙啓 ClassID
-        ccols[4] = r.buddyId.c_str();
+        std::vector<CString> ccols(colCount);
+        ccols[4] = r.buddyId.c_str(); // 鍗犱綅
         auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
-        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
+        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), zebraBk(zebra));
 
-        consumed.insert(r.classId);
+        consumed.insert(makeKeyR(r));
         ++zebra;
     }
 
     // -------- Phase 2: 鍓╀綑鏈秷璐圭殑锛屼綔涓衡�滃崟鏉℃牴琛屸�� ----------
-    for (size_t i = 0; i < page.items.size(); ++i) {
-        const auto& r = page.items[i];
-        if (consumed.count(r.classId)) continue;
+    for (size_t i = 0; i < pageRef.items.size(); ++i) {
+        const auto& r = pageRef.items[i];
+        if (consumed.count(makeKeyR(r))) continue;
 
         COLORREF bk = zebraBk(zebra);
 
+        std::vector<CString> cols(colCount);
+        cols[1] = std::to_string(r.id).c_str();
+        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
+        cols[3] = std::to_string(r.jobSeqNo).c_str();
+        cols[4] = r.classId.c_str();
+        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
+        cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
+        cols[7] = r.tStart.c_str();
+        cols[8] = r.tEnd.c_str();
+        cols[9] = r.buddyId.c_str();
+        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
+        cols[11] = r.path.c_str();
+        cols[12] = r.params.c_str();
+
+        auto* n = m_listCtrl.InsertRoot(cols);
+        m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
+
+        consumed.insert(makeKeyR(r));
+        ++zebra;
+    }
+
+    // 涓�娆℃�ч噸缁�
+    m_listCtrl.RebuildVisible();
+
+#else
+    // ===== DEMO 鍒嗘敮锛堜繚鎸佸師鏍凤紱鑻ヨ婕旂ず鍚屾牱閫昏緫锛屽彲浠跨収涓婇潰鏀归�狅級=====
+    // 濡傛灉澶氬嚭涓�鏉★紝鐪嬬湅瀹冩槸鍚︽槸鈥滄湰椤垫渶鍚庝竴鏉♀�濈殑 buddy
+    std::optional<decltype(page.items)::value_type> lookahead;
+    auto iEquals = [](const std::string& a, const std::string& b) {
+#ifdef _WIN32
+        return _stricmp(a.c_str(), b.c_str()) == 0;
+#else
+        return strcasecmp(a.c_str(), b.c_str()) == 0;
+#endif
+    };
+
+    if (page.items.size() == rawLimit) {
+        const auto& last = page.items[PAGE_SIZE - 1];
+        const auto& extra = page.items[PAGE_SIZE];
+        bool pair =
+            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) ||
+            (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId));
+        if (pair) lookahead = extra;
+        page.items.pop_back();
+    }
+
+    // 浣犲彲浠ユ妸 DEMO 鍒嗘敮涔熷垏鍒颁笁鍏冮敭閫昏緫锛涜繖閲屼粠鐣�
+    auto& pageRef = page;
+    std::unordered_map<std::string, size_t> idxById;
+    idxById.reserve(pageRef.items.size());
+    for (size_t i = 0; i < pageRef.items.size(); ++i) idxById[pageRef.items[i].classId] = i;
+
+    std::unordered_set<std::string> consumed;
+    int zebra = 0;
+    auto zebraBk = [&](int z) -> COLORREF {
+        return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
+    };
+
+    for (size_t i = 0; i < pageRef.items.size(); ++i) {
+        const auto& r = pageRef.items[i];
+        if (consumed.count(r.classId)) continue;
+        if (!r.buddyId.empty()) {
+            auto it = idxById.find(r.buddyId);
+            if (it != idxById.end()) {
+                const auto& br = pageRef.items[it->second];
+                if (!consumed.count(br.classId)) {
+                    COLORREF bk = zebraBk(zebra);
+                    std::vector<CString> pcols(colCount), ccols(colCount);
+                    pcols[1] = std::to_string(r.id).c_str();
+                    pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
+                    pcols[3] = std::to_string(r.jobSeqNo).c_str();
+                    pcols[4] = r.classId.c_str();
+                    pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
+                    pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
+                    pcols[7] = r.tStart.c_str();
+                    pcols[8] = r.tEnd.c_str();
+                    pcols[9] = r.buddyId.c_str();
+                    pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
+                    pcols[11] = r.path.c_str();
+                    pcols[12] = r.params.c_str();
+                    auto* nParent = m_listCtrl.InsertRoot(pcols);
+                    MaybeRestoreExpandByKey(nParent, expandedKeys);
+                    m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
+
+                    ccols[1] = std::to_string(br.id).c_str();
+                    ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
+                    ccols[3] = std::to_string(br.jobSeqNo).c_str();
+                    ccols[4] = br.classId.c_str();
+                    ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
+                    ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
+                    ccols[7] = br.tStart.c_str();
+                    ccols[8] = br.tEnd.c_str();
+                    ccols[9] = br.buddyId.c_str();
+                    ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
+                    ccols[11] = br.path.c_str();
+                    ccols[12] = br.params.c_str();
+                    auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
+                    m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
+
+                    consumed.insert(r.classId);
+                    consumed.insert(br.classId);
+                    ++zebra;
+                    continue;
+                }
+            }
+
+            // 鎻掑崰浣嶅瓙
+            COLORREF bk = zebraBk(zebra);
+            std::vector<CString> pcols(colCount), ccols(colCount);
+            pcols[1] = std::to_string(r.id).c_str();
+            pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
+            pcols[3] = std::to_string(r.jobSeqNo).c_str();
+            pcols[4] = r.classId.c_str();
+            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
+            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
+            pcols[7] = r.tStart.c_str();
+            pcols[8] = r.tEnd.c_str();
+            pcols[9] = r.buddyId.c_str();
+            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
+            pcols[11] = r.path.c_str();
+            pcols[12] = r.params.c_str();
+            auto* nParent = m_listCtrl.InsertRoot(pcols);
+            MaybeRestoreExpandByKey(nParent, expandedKeys);
+            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
+
+            ccols[4] = r.buddyId.c_str();
+            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
+            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
+
+            consumed.insert(r.classId);
+            ++zebra;
+        }
+    }
+    for (size_t i = 0; i < pageRef.items.size(); ++i) {
+        const auto& r = pageRef.items[i];
+        if (consumed.count(r.classId)) continue;
+
+        COLORREF bk = zebraBk(zebra);
         std::vector<CString> cols(colCount);
         cols[1] = std::to_string(r.id).c_str();
         cols[2] = std::to_string(r.cassetteSeqNo).c_str();
@@ -816,8 +1064,8 @@
         ++zebra;
     }
 
-    // 涓�娆℃�ч噸缁�
     m_listCtrl.RebuildVisible();
+#endif
 
     // 涓婁竴椤� / 涓嬩竴椤�
     UpdatePageControls();
@@ -1045,6 +1293,274 @@
     }
 }
 
+void CPageGlassList::OnBnClickedButtonExportRow()
+{
+    int nSelected = m_listCtrl.GetSelectionMark();
+    if (nSelected == -1) {
+        AfxMessageBox(_T("璇峰厛閫夋嫨涓�琛岃褰曪紒"));
+        return;
+    }
+
+    // 鐩存帴浠庣涓�鍒楄幏鍙� ID
+    CString strId = m_listCtrl.GetItemText(nSelected, 1);
+
+    if (strId.IsEmpty()) {
+        AfxMessageBox(_T("WIP璁板綍鏆備笉鏀寔淇濆瓨"));
+        return;
+    }
+
+    // 鏁版嵁搴撹褰�
+    long long recordId = _ttoi64(strId);
+
+    // 浠庢暟鎹簱鏌ヨ瀹屾暣璁板綍
+    auto& db = GlassLogDb::Instance();
+    auto row = db.queryById(recordId);
+
+    if (!row) {
+        AfxMessageBox(_T("鏌ヨ璁板綍澶辫触"));
+        return;
+    }
+
+    // 浣跨敤 Glass ID 鏋勫缓榛樿鏂囦欢鍚�
+    CString strDefaultFileName;
+    CString strGlassId = row->classId.c_str();
+
+    // 绉婚櫎鏂囦欢鍚嶄腑鐨勯潪娉曞瓧绗�
+    CString strSanitizedGlassId = strGlassId;
+    strSanitizedGlassId.Remove('\\');
+    strSanitizedGlassId.Remove('/');
+    strSanitizedGlassId.Remove(':');
+    strSanitizedGlassId.Remove('*');
+    strSanitizedGlassId.Remove('?');
+    strSanitizedGlassId.Remove('"');
+    strSanitizedGlassId.Remove('<');
+    strSanitizedGlassId.Remove('>');
+    strSanitizedGlassId.Remove('|');
+
+    strDefaultFileName.Format(_T("Glass_%s.csv"), strSanitizedGlassId);
+
+    // 鏂囦欢淇濆瓨瀵硅瘽妗嗭紝璁剧疆榛樿鏂囦欢鍚�
+    CFileDialog fileDialog(FALSE, _T("csv"), strDefaultFileName,
+        OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
+        _T("CSV Files (*.csv)|*.csv|JSON Files (*.json)|*.json||"));
+
+    if (fileDialog.DoModal() != IDOK) return;
+
+    CString filePath = fileDialog.GetPathName();
+    CString fileExt = fileDialog.GetFileExt();
+
+    if (fileExt.CompareNoCase(_T("json")) == 0) {
+        ExportToJson(*row, filePath);
+    }
+    else {
+        ExportToCsv(*row, filePath);
+    }
+}
+
+void CPageGlassList::ExportToJson(const GlassLogDb::Row& row, const CString& filePath)
+{
+    // 淇濆瓨涓� JSON
+    if (!row.pretty.empty()) {
+        CFile file;
+        if (file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
+            file.Write(row.pretty.c_str(), row.pretty.length());
+            file.Close();
+
+            CString strSuccess;
+            strSuccess.Format(_T("璁板綍宸蹭繚瀛樹负JSON鏂囦欢锛歕n%s"), filePath);
+            AfxMessageBox(strSuccess);
+        }
+        else {
+            AfxMessageBox(_T("淇濆瓨鏂囦欢澶辫触"));
+        }
+    }
+    else {
+        AfxMessageBox(_T("璇ヨ褰曟病鏈塉SON鏁版嵁"));
+    }
+}
+
+void CPageGlassList::ExportToCsv(const GlassLogDb::Row& row, const CString& filePath)
+{
+    CString csvContent;
+
+    // === 绗竴閮ㄥ垎锛氬熀纭�淇℃伅 ===
+    ExportBasicInfo(csvContent, row);
+
+    // === 绗簩閮ㄥ垎锛氬伐鑹哄弬鏁� ===
+    ExportProcessParams(csvContent, row);
+
+    // === 绗笁閮ㄥ垎锛氫紶鎰熷櫒鏁版嵁璇︽儏 ===
+    ExportSensorData(csvContent, row);
+
+    // 浣跨敤杈呭姪鍑芥暟淇濆瓨涓� UTF-8 缂栫爜
+    if (WriteAnsiStringAsUtf8ToFile(csvContent, filePath)) {
+        CString strSuccess;
+        strSuccess.Format(_T("璁板綍宸蹭繚瀛樹负CSV鏂囦欢锛歕n%s"), filePath);
+        AfxMessageBox(strSuccess);
+    }
+    else {
+        AfxMessageBox(_T("淇濆瓨鏂囦欢澶辫触"));
+    }
+}
+
+void CPageGlassList::ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row)
+{
+    csvContent += _T("=== 鍩虹淇℃伅 ===\n");
+    csvContent += _T("ID,Cassette搴忓垪鍙�,Job搴忓垪鍙�,Glass ID,鐗╂枡绫诲瀷,鐘舵��,寮�濮嬫椂闂�,缁撴潫鏃堕棿,缁戝畾Glass ID,AOI缁撴灉,璺緞\n");
+
+    CString baseInfoRow;
+    baseInfoRow.Format(_T("%lld,%d,%d,%s,%d,%d,%s,%s,%s,%d,%s\n"),
+        row.id, row.cassetteSeqNo, row.jobSeqNo,
+        CString(row.classId.c_str()), row.materialType, row.state,
+        CString(row.tStart.c_str()), CString(row.tEnd.c_str()),
+        CString(row.buddyId.c_str()), row.aoiResult,
+        CString(row.path.c_str()));
+    csvContent += baseInfoRow;
+}
+
+void CPageGlassList::ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row)
+{
+    csvContent += _T("\n=== 宸ヨ壓鍙傛暟 ===\n");
+
+    // 濡傛灉鏈� pretty 瀛楁锛岃В鏋愬伐鑹哄弬鏁�
+    if (!row.pretty.empty()) {
+        SERVO::CGlass tempGlass;
+        if (GlassJson::FromString(row.pretty, tempGlass)) {
+            auto& params = tempGlass.getParams();
+            if (!params.empty()) {
+                // 宸ヨ壓鍙傛暟琛ㄥご
+                csvContent += _T("鍙傛暟鍚嶇О,鍙傛暟ID,鏁板��,鏈哄櫒鍗曞厓\n");
+
+                // 宸ヨ壓鍙傛暟鏁版嵁
+                for (auto& param : params) {
+                    CString paramRow;
+                    CString valueStr;
+
+                    // 鏍规嵁鍙傛暟绫诲瀷鏍煎紡鍖栨暟鍊�
+                    if (param.getValueType() == PVT_INT) {
+                        valueStr.Format(_T("%d"), param.getIntValue());
+                    }
+                    else {
+                        valueStr.Format(_T("%.3f"), param.getDoubleValue());
+                    }
+
+                    paramRow.Format(_T("%s,%s,%s,%s\n"),
+                        CString(param.getName().c_str()),
+                        CString(param.getId().c_str()),
+                        valueStr,
+                        CString(param.getUnit().c_str()));
+
+                    csvContent += paramRow;
+                }
+            }
+            else {
+                csvContent += _T("鏃犲伐鑹哄弬鏁版暟鎹甛n");
+            }
+        }
+        else {
+            csvContent += _T("鏃犳硶瑙f瀽宸ヨ壓鍙傛暟\n");
+        }
+    }
+    else {
+        csvContent += _T("鏃犲伐鑹哄弬鏁版暟鎹甛n");
+    }
+}
+
+void CPageGlassList::ExportSensorData(CString& csvContent, const GlassLogDb::Row& row)
+{
+    csvContent += _T("\n=== 浼犳劅鍣ㄦ暟鎹鎯� ===\n");
+
+    // 濡傛灉鏈� pretty 瀛楁锛岃В鏋愪紶鎰熷櫒鏁版嵁
+    if (!row.pretty.empty()) {
+        SERVO::CGlass tempGlass;
+        if (GlassJson::FromString(row.pretty, tempGlass)) {
+#if USE_MOCK_SENSOR_DATA
+            // 鐢熸垚妯℃嫙鐨凷VData鐢ㄤ簬娴嬭瘯
+            GenerateMockSVData(tempGlass);
+#endif
+            // 瀵规瘡涓満鍣ㄧ敓鎴愯〃鏍�
+            for (const auto& machinePair : tempGlass.getAllSVData()) {
+                int machineId = machinePair.first;
+                CString machineName = CString(SERVO::CServoUtilsTool::getEqName(machineId).c_str());
+
+                csvContent += _T("\n[") + machineName + _T("]\n");
+
+                // 鑾峰彇璇ユ満鍣ㄧ殑棰勫畾涔夊垪椤哄簭
+                auto columnOrder = getMachineColumnOrder(machineId);
+
+                if (columnOrder.empty()) {
+                    csvContent += _T("鏃犻瀹氫箟鍒楅厤缃甛n");
+                    continue;
+                }
+
+                // 鏋勫缓琛ㄥご - 鐩存帴浣跨敤涓枃鍒楀悕
+                CString header = _T("鏃堕棿鎴�(ms),鏈湴鏃堕棿");
+                for (const auto& dataType : columnOrder) {
+                    header += _T(",");
+                    header += CString(dataType.c_str()); // 鐩存帴浣跨敤涓枃鍒楀悕
+                }
+                header += _T("\n");
+                csvContent += header;
+
+                // 妫�鏌ユ槸鍚︽湁鏁版嵁
+                if (machinePair.second.empty()) {
+                    csvContent += _T("鏃犱紶鎰熷櫒鏁版嵁\n");
+                    continue;
+                }
+
+                // 浣跨敤绗竴涓暟鎹被鍨嬬殑鏃堕棿搴忓垪浣滀负鍩哄噯
+                const std::string& firstDataType = columnOrder[0];
+                auto firstDataTypeIt = machinePair.second.find(firstDataType);
+                if (firstDataTypeIt == machinePair.second.end() || firstDataTypeIt->second.empty()) {
+                    csvContent += _T("鏃犲熀鍑嗘暟鎹被鍨嬫暟鎹甛n");
+                    continue;
+                }
+
+                const auto& timeSeries = firstDataTypeIt->second;
+
+                // 瀵逛簬姣忎釜鏃堕棿鐐癸紝杈撳嚭涓�琛屾暟鎹�
+                for (size_t i = 0; i < timeSeries.size(); i++) {
+                    auto timestamp = timeSeries[i].timestamp;
+
+                    // 鏃堕棿鎴筹紙姣锛�
+                    auto ms = timePointToMs(timestamp);
+                    CString row;
+                    row.Format(_T("%lld,"), ms);
+
+                    // 鏈湴鏃堕棿瀛楃涓�
+                    CString localTime = CString(timePointToString(timestamp).c_str());
+                    row += localTime;
+
+                    // 鎸夌収棰勫畾涔夌殑鍒楅『搴忚緭鍑烘暟鎹�
+                    for (const auto& dataType : columnOrder) {
+                        row += _T(",");
+
+                        auto dataTypeIt = machinePair.second.find(dataType);
+                        if (dataTypeIt != machinePair.second.end() && i < dataTypeIt->second.size()) {
+                            // 鐩存帴鎸夌储寮曡幏鍙栨暟鎹�
+                            CString valueStr;
+                            valueStr.Format(_T("%.3f"), dataTypeIt->second[i].value);
+                            row += valueStr;
+                        }
+                        else {
+                            // 鐞嗚涓婁笉搴旇鍙戠敓锛屽洜涓烘偍璇存病鏈夌┖鍊�
+                            row += _T("N/A");
+                        }
+                    }
+                    row += _T("\n");
+                    csvContent += row;
+                }
+            }
+        }
+        else {
+            csvContent += _T("鏃犳硶瑙f瀽浼犳劅鍣ㄦ暟鎹甛n");
+        }
+    }
+    else {
+        csvContent += _T("鏃犱紶鎰熷櫒鏁版嵁\n");
+    }
+}
+
 void CPageGlassList::OnBnClickedButtonPrevPage()
 {
     if (m_nCurPage > 1) {
@@ -1162,7 +1678,7 @@
     CExpandableListCtrl::Node* savedTop = nullptr;
 
     // 3) 閫愪釜澶勭悊 WIP锛氬凡瀛樺湪 -> 灏卞湴鏇存柊锛涘繀瑕佹椂鈥滃彧瀵规牴琛ュ瓙椤光��
-    //                 涓嶅瓨鍦� -> 浼樺厛鎸傚埌 buddy 瀹瑰櫒锛涘惁鍒欒Е鍙戞暣椤甸噸寤猴紙鏂版牴淇濇寔椤堕儴锛�
+    //                 涓嶅瓨鍦� -> 鎸傚埌 buddy 瀹瑰櫒锛涜嫢 buddy 涓嶅湪鍙琛紝瑙﹀彂鍏ㄩ噺閲嶅缓锛堜繚璇� WIP 椤堕儴锛�
     for (auto* g : wipGlasses) {
         if (!GlassMatchesFilters(*g, m_filters)) continue;
 
@@ -1205,7 +1721,7 @@
                 }
             }
 
-            // 鈥斺�� 鍙鈥滄牴鑺傜偣鈥濊ˉ瀛愰」锛屼笖浠呭綋 buddy 灏氭湭鍑虹幇鍦ㄥ彲瑙佽〃锛屼笖鏍逛笅涔熸病鏈夎 buddy 鈥斺�� 
+            // 鈥斺�� 鍙鈥滄牴鑺傜偣鈥濊ˉ瀛愰」 鈥斺�� 
             SERVO::CGlass* b = g->getBuddy();
             if (b) {
                 auto itRoot = wipRootById.find(cid);
@@ -1227,7 +1743,7 @@
                     bool buddyExistsAnywhere = (wipRowById.find(newBid) != wipRowById.end());
                     bool hasChildAlready = NodeHasChildWithClassId(container, newBuddyCid);
 
-                    // 鍏崇郴鏄惁鍙戠敓鍙樺寲锛燂紙oldChildCid 涓� newBuddyCid 涓嶅悓锛屾垨鏈夊瓙浣嗙幇鍦ㄦ病 buddy锛�
+                    // 鍏崇郴鏄惁鍙戠敓鍙樺寲锛�
                     bool relationChanged =
                         (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||
                         (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||
@@ -1270,10 +1786,9 @@
                         }
                     }
                 }
-                // 鑻ュ綋鍓嶆槸鈥滃瓙鑺傜偣鈥濓紝涓嶅湪杩欓噷璋冩暣鐖跺瓙鍏崇郴锛涜鈥滃叧绯诲彉鍖栤�濊蛋鍏ㄩ噺閲嶅缓
             }
             else {
-                // 娌℃湁 buddy锛氬鏋滃鍣ㄤ笅鐜板湪鏈夊瓙锛屼篃绠楀叧绯诲彉鍖栵紝瑙﹀彂閲嶅缓
+                // 娌� buddy 浣嗗鍣ㄤ笅鏈夊瓙 -> 鍏崇郴鍙樺寲锛岃Е鍙戝叏閲忛噸寤�
                 auto itRoot = wipRootById.find(cid);
                 if (itRoot != wipRootById.end()) {
                     CExpandableListCtrl::Node* container = itRoot->second;
@@ -1284,8 +1799,6 @@
         }
         else {
             // (B) 涓嶅瓨鍦細鏂板
-            //   鍏堝皾璇曗�滄寕鍒� buddy 鐨勫鍣ㄦ牴鈥濅笅闈紱
-            //   鑻� buddy 涓嶅湪褰撳墠鍙琛紝鍒欒Е鍙戝叏閲忛噸寤猴紙淇濊瘉 WIP 椤堕儴锛夈��
             SERVO::CGlass* b = g->getBuddy();
             CExpandableListCtrl::Node* container = nullptr;
 
@@ -1303,7 +1816,6 @@
             }
 
             if (container) {
-                // buddy 瀹瑰櫒瀛樺湪锛氭妸 g 浣滀负鈥滃瓙琛屸�濇寕涓婂幓锛堥伩鍏嶉噸澶嶏級
                 CString cidCs = g->getID().c_str();
                 if (!NodeHasChildWithClassId(container, cidCs)) {
                     if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
@@ -1405,3 +1917,116 @@
 
     return CDialogEx::PreTranslateMessage(pMsg);
 }
+
+// 鑾峰彇鏈哄櫒棰勫畾涔夌殑鍒楅『搴�
+std::vector<std::string> CPageGlassList::getMachineColumnOrder(int machineId)
+{
+    auto dataTypes = SERVO::CServoUtilsTool::getEqDataTypes();
+    auto it = dataTypes.find(machineId);
+    return it != dataTypes.end() ? it->second : std::vector<std::string>();
+}
+
+// 鏃堕棿鎴宠浆鎹负瀛楃涓�
+std::string CPageGlassList::timePointToString(const std::chrono::system_clock::time_point& tp) 
+{
+    auto time_t = std::chrono::system_clock::to_time_t(tp);
+    std::tm tm;
+    localtime_s(&tm, &time_t);
+    char buffer[20];
+    std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
+    return buffer;
+}
+
+// 鏃堕棿鎴宠浆鎹负姣
+int64_t CPageGlassList::timePointToMs(const std::chrono::system_clock::time_point& tp)
+{
+    return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
+}
+
+// 鐢熸垚妯℃嫙鐨凷VData鐢ㄤ簬娴嬭瘯
+void CPageGlassList::GenerateMockSVData(SERVO::CGlass& glass)
+{
+    // 鑾峰彇璁惧鏁版嵁绫诲瀷閰嶇疆
+    auto& dataTypes = SERVO::CServoUtilsTool::getEqDataTypes();
+    
+    // 涓烘瘡涓澶囩敓鎴愭ā鎷熸暟鎹�
+    for (const auto& machinePair : dataTypes) {
+        int machineId = machinePair.first;
+        const auto& dataTypeList = machinePair.second;
+        
+        // 鐢熸垚鏃堕棿搴忓垪锛氫粠褰撳墠鏃堕棿寰�鍓嶆帹10鍒嗛挓锛屾瘡1绉掍竴涓暟鎹偣
+        auto now = std::chrono::system_clock::now();
+        auto startTime = now - std::chrono::minutes(10);
+        
+        // 涓烘瘡涓暟鎹被鍨嬬敓鎴愭ā鎷熸暟鎹�
+        for (const auto& dataType : dataTypeList) {
+            std::vector<SERVO::SVDataItem> mockData;
+            
+            // 鐢熸垚600涓暟鎹偣锛�10鍒嗛挓 * 60涓偣/鍒嗛挓锛�
+            for (int i = 0; i < 600; ++i) {
+                auto timestamp = startTime + std::chrono::seconds(i * 1);
+                
+                // 鏍规嵁璁惧绫诲瀷鍜屾暟鎹被鍨嬬敓鎴愪笉鍚岀殑妯℃嫙鍊�
+                double value = GenerateMockValue(machineId, dataType, i);
+                
+                mockData.emplace_back(timestamp, value);
+            }
+            
+            // 灏嗘ā鎷熸暟鎹坊鍔犲埌glass瀵硅薄涓�
+            glass.addSVData(machineId, dataType, mockData);
+        }
+    }
+}
+
+// 鏍规嵁璁惧绫诲瀷鍜屾暟鎹被鍨嬬敓鎴愭ā鎷熸暟鍊�
+double CPageGlassList::GenerateMockValue(int machineId, const std::string& dataType, int index)
+{
+    // 鍩虹鍊艰寖鍥�
+    double baseValue = 0.0;
+    double variation = 0.0;
+    
+    // 鏍规嵁璁惧绫诲瀷璁剧疆鍩虹鍊�
+    switch (machineId) {
+        case EQ_ID_Bonder1:
+        case EQ_ID_Bonder2:
+            if (dataType.find("鍘嬪姏") != std::string::npos) {
+                baseValue = 50.0;  // 鍘嬪姏鍩虹鍊�
+                variation = 10.0;  // 鍘嬪姏鍙樺寲鑼冨洿
+            } else if (dataType.find("娓╁害") != std::string::npos) {
+                baseValue = 180.0; // 娓╁害鍩虹鍊�
+                variation = 5.0;   // 娓╁害鍙樺寲鑼冨洿
+            } else if (dataType.find("鎵╁睍鍊�") != std::string::npos) {
+                baseValue = 100.0; // 鎵╁睍鍊煎熀纭�鍊�
+                variation = 15.0;  // 鎵╁睍鍊煎彉鍖栬寖鍥�
+            }
+            break;
+            
+        case EQ_ID_VACUUMBAKE:
+            if (dataType.find("鎵╁睍鍊�") != std::string::npos) {
+                baseValue = 80.0;
+                variation = 12.0;
+            } else if (dataType.find("娓╁害") != std::string::npos) {
+                baseValue = 200.0;
+                variation = 8.0;
+            }
+            break;
+            
+        case EQ_ID_BAKE_COOLING:
+            if (dataType.find("娓╁害") != std::string::npos) {
+                baseValue = 25.0;  // 鍐峰嵈娓╁害
+                variation = 3.0;
+            }
+            break;
+            
+        default:
+            baseValue = 50.0;
+            variation = 5.0;
+            break;
+    }
+    
+    // 娣诲姞鏃堕棿鐩稿叧鐨勮秼鍔垮拰闅忔満鍙樺寲
+    double timeTrend = sin(index * 0.1) * 2.0;  // 姝e鸡娉㈣秼鍔�
+    double randomNoise = (rand() % 100 - 50) / 100.0 * variation * 0.3;  // 闅忔満鍣0
+    
+    return baseValue + timeTrend + randomNoise;
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/CPageGlassList.h b/SourceCode/Bond/Servo/CPageGlassList.h
index 1094c50..09e548a 100644
--- a/SourceCode/Bond/Servo/CPageGlassList.h
+++ b/SourceCode/Bond/Servo/CPageGlassList.h
@@ -2,6 +2,10 @@
 #include "CExpandableListCtrl.h"
 #include "GlassLogDb.h"
 
+// ====== 缂栬瘧寮�鍏宠鏄� ======
+// USE_MOCK_SENSOR_DATA: 1=鍚敤妯℃嫙浼犳劅鍣ㄦ暟鎹敓鎴愶紱0=浣跨敤鐪熷疄鏁版嵁
+// 鍦� CPageGlassList.cpp 涓畾涔�
+
 
 // CPageGlassList 瀵硅瘽妗�
 
@@ -55,6 +59,17 @@
 	void UpdateWipData();
 	bool eraseGlassInVector(SERVO::CGlass* pGlass, std::vector<SERVO::CGlass*>& glasses);
 	void UpdateWipRow(unsigned int index, SERVO::CGlass* pGlass);
+	bool WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath);
+	void ExportToCsv(const GlassLogDb::Row& row, const CString& filePath);
+	void ExportToJson(const GlassLogDb::Row& row, const CString& filePath);
+	void ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row);
+	void ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row);
+	void ExportSensorData(CString& csvContent, const GlassLogDb::Row& row);
+	static std::vector<std::string> getMachineColumnOrder(int machineId);
+	static std::string timePointToString(const std::chrono::system_clock::time_point& tp);
+	static int64_t timePointToMs(const std::chrono::system_clock::time_point& tp);
+	void GenerateMockSVData(SERVO::CGlass& glass);
+	double GenerateMockValue(int machineId, const std::string& dataType, int index);
 
 // 瀵硅瘽妗嗘暟鎹�
 #ifdef AFX_DESIGN_TIME
@@ -77,4 +92,6 @@
 	afx_msg void OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult);
 	virtual BOOL PreTranslateMessage(MSG* pMsg);
 	DECLARE_MESSAGE_MAP()
+public:
+	afx_msg void OnBnClickedButtonExportRow();
 };
diff --git a/SourceCode/Bond/Servo/CPagePortProperty.cpp b/SourceCode/Bond/Servo/CPagePortProperty.cpp
index 0c241c9..3b8f4e5 100644
--- a/SourceCode/Bond/Servo/CPagePortProperty.cpp
+++ b/SourceCode/Bond/Servo/CPagePortProperty.cpp
@@ -196,8 +196,12 @@
 	CMsgDlg msgDlg("璇风瓑寰�", "姝e湪鎿嶄綔锛岃绛夊緟...");
 	msgDlg.SetData((DWORD_PTR)this);
 
+	// 淇敼涓哄彧淇濆瓨鍦ㄦ湰鍦伴厤缃�
 	ASSERT(m_pPort != nullptr);
 	int index = ((CComboBox*)GetDlgItem(IDC_COMBO_PORT_TYPE))->GetCurSel();
+	theApp.m_model.setPortType(m_pPort->getIndex(), SERVO::PortType(index + 1));
+
+	/*
 	m_pPort->setPortType(SERVO::PortType(index + 1), [&](int code) -> int {
 		Sleep(100);
 		CString strMsg;
@@ -234,6 +238,7 @@
 
 	msgDlg.DoModal();
 	g_nMsgDlgShow = 1;
+	*/
 }
 
 void CPagePortProperty::OnCbnSelchangeComboPortMode()
diff --git a/SourceCode/Bond/Servo/CPageReport.cpp b/SourceCode/Bond/Servo/CPageReport.cpp
index 94f55b8..0066c9b 100644
--- a/SourceCode/Bond/Servo/CPageReport.cpp
+++ b/SourceCode/Bond/Servo/CPageReport.cpp
@@ -108,7 +108,6 @@
 	CHMPropertyPage::OnSize(nType, cx, cy);
 	if (GetDlgItem(IDC_LIST1) == nullptr) return;
 
-	CWnd* pItem;
 	CRect rcClient, rcItem;
 	GetClientRect(&rcClient);
 	m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
diff --git a/SourceCode/Bond/Servo/CPageVarialbles.cpp b/SourceCode/Bond/Servo/CPageVarialbles.cpp
index 415a5dc..5f4f5cc 100644
--- a/SourceCode/Bond/Servo/CPageVarialbles.cpp
+++ b/SourceCode/Bond/Servo/CPageVarialbles.cpp
@@ -108,7 +108,6 @@
 
 	if (GetDlgItem(IDC_LIST1) == nullptr) return;
 
-	CWnd* pItem;
 	CRect rcClient, rcItem;
 	GetClientRect(&rcClient);
 	m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
diff --git a/SourceCode/Bond/Servo/CParam.cpp b/SourceCode/Bond/Servo/CParam.cpp
index 4935b4e..57a8f0c 100644
--- a/SourceCode/Bond/Servo/CParam.cpp
+++ b/SourceCode/Bond/Servo/CParam.cpp
@@ -65,7 +65,12 @@
 
 double CParam::getDoubleValue()
 {
-	return m_fValue;
+	if(m_nValueType == PVT_DOUBLE)
+		return m_fValue;
+	if (m_nValueType == PVT_INT)
+		return (double)m_nValue;
+
+	return 0.0;
 }
 
 void CParam::setDoubleValue(double value)
diff --git a/SourceCode/Bond/Servo/CServoUtilsTool.cpp b/SourceCode/Bond/Servo/CServoUtilsTool.cpp
index 1bf444a..c9d06f6 100644
--- a/SourceCode/Bond/Servo/CServoUtilsTool.cpp
+++ b/SourceCode/Bond/Servo/CServoUtilsTool.cpp
@@ -4,6 +4,32 @@
 
 
 namespace SERVO {
+	static std::unordered_map<int, std::vector<std::string>> EQ_DATA_TYPES = {
+		{EQ_ID_Bonder1, {
+			"气囊压力", "上腔压力", "管道真空规值", "腔体真空规值",
+			"上腔温度1", "上腔温度2", "上腔温度3", "上腔温度4",
+			"上腔温度5", "上腔温度6", "下腔温度1", "下腔温度2",
+			"下腔温度3", "下腔温度4", "下腔温度5", "下腔温度6"
+		}},
+		{EQ_ID_Bonder2, {
+			"气囊压力", "上腔压力", "管道真空规值", "腔体真空规值",
+			"上腔温度1", "上腔温度2", "上腔温度3", "上腔温度4",
+			"上腔温度5", "上腔温度6", "下腔温度1", "下腔温度2",
+			"下腔温度3", "下腔温度4", "下腔温度5", "下腔温度6"
+		}},
+		{EQ_ID_VACUUMBAKE, {
+			"A腔真空规值", "A腔温控1", "A腔温控2", "A腔温控4",
+			"A腔温控5", "A腔温控6", "A腔温控7", "B腔真空规值",
+			"B腔温控1", "B腔温控2", "B腔温控4", "B腔温控5",
+			"B腔温控6", "B腔温控7"
+		}},
+		{EQ_ID_BAKE_COOLING, {
+			"A烘烤温控1", "A烘烤温控2", "A烘烤温控4", "A烘烤温控5",
+			"A烘烤温控6", "A烘烤温控7", "B烘烤温控1", "B烘烤温控2",
+			"B烘烤温控4", "B烘烤温控5", "B烘烤温控6", "B烘烤温控7"
+		}}
+	};
+
 	CServoUtilsTool::CServoUtilsTool()
 	{
 
@@ -12,6 +38,43 @@
 	CServoUtilsTool::~CServoUtilsTool()
 	{
 
+	}
+
+	std::string CServoUtilsTool::getEqName(int eqid)
+	{
+		switch (eqid)
+		{
+		case EQ_ID_LOADPORT1:
+			return "Port1";
+		case EQ_ID_LOADPORT2:
+			return "Port2";
+		case EQ_ID_LOADPORT3:
+			return "Port3";
+		case EQ_ID_LOADPORT4:
+			return "Port4";
+		case EQ_ID_ALIGNER:
+			return "Aligner";
+		case EQ_ID_FLIPER:
+			return "Fliper";
+		case EQ_ID_VACUUMBAKE:
+			return "VacuumBake";
+		case EQ_ID_Bonder1:
+			return "Bonder1";
+		case EQ_ID_Bonder2:
+			return "Bonder2";
+		case EQ_ID_BAKE_COOLING:
+			return "BakeCooling";
+		case EQ_ID_MEASUREMENT:
+			return "AOI";
+		case EQ_ID_ARM_TRAY1:
+			return "ArmTray1";
+		case EQ_ID_ARM_TRAY2:
+			return "ArmTray2";
+		default:
+			break;
+		}
+
+		return "";
 	}
 
 	std::string CServoUtilsTool::getEqUnitName(int eqid, int unit)
@@ -39,11 +102,6 @@
 			if (unit == 1) return "烘烤B腔";
 		}
 
-		if (eqid == EQ_ID_VACUUMBAKE) {
-			if (unit == 0) return "烘烤A腔";
-			if (unit == 1) return "烘烤B腔";
-		}
-
 		if (eqid == EQ_ID_Bonder1) {
 			return "Bonder1";
 		}
@@ -56,8 +114,8 @@
 
 			if (unit == 0) return "后烘烤A腔";
 			if (unit == 1) return "冷却A";
-			if (unit == 0) return "后烘烤B腔";
-			if (unit == 1) return "冷却B";
+			if (unit == 2) return "后烘烤B腔";
+			if (unit == 3) return "冷却B";
 		}
 
 		if (eqid == EQ_ID_MEASUREMENT) {
@@ -203,4 +261,9 @@
 			break;
 		}
 	}
+
+	std::unordered_map<int, std::vector<std::string>>& CServoUtilsTool::getEqDataTypes()
+	{
+		return EQ_DATA_TYPES;
+	}
 }
diff --git a/SourceCode/Bond/Servo/CServoUtilsTool.h b/SourceCode/Bond/Servo/CServoUtilsTool.h
index 945d717..2945984 100644
--- a/SourceCode/Bond/Servo/CServoUtilsTool.h
+++ b/SourceCode/Bond/Servo/CServoUtilsTool.h
@@ -11,11 +11,13 @@
 		virtual ~CServoUtilsTool();
 
 	public:
+		static std::string getEqName(int eqid);
 		static std::string getEqUnitName(int eqid, int unit);
 		static std::string getEqUnitName(int eqid, int unit, int slot);
 		static std::string getMaterialsTypeText(MaterialsType type);
 		static std::string getGlassStateText(SERVO::GlsState state);
 		static std::string getInspResultText(SERVO::InspResult result);
+		static std::unordered_map<int, std::vector<std::string>>& getEqDataTypes();
 	};
 }
 
diff --git a/SourceCode/Bond/Servo/CVacuumBake.cpp b/SourceCode/Bond/Servo/CVacuumBake.cpp
index ea3bf70..3ad0dbd 100644
--- a/SourceCode/Bond/Servo/CVacuumBake.cpp
+++ b/SourceCode/Bond/Servo/CVacuumBake.cpp
@@ -482,7 +482,58 @@
 
 	int CVacuumBake::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
 	{
-		return parsingParams(pszData, size, params);
+		ASSERT(pszData);
+		if (size < 250) return 0;
+		int i = 0, v;
+
+
+		// 1.工艺参数序号
+		v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+		params.push_back(CParam("工艺参数序号", "", this->getName().c_str(), v));
+		i += 2;
+
+		if (v == 1) {
+			// 2.A_腔加热时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("A_腔加热时间", "", this->getName().c_str(), v * 0.1f));
+			i += 2;
+
+			// 3.A_腔破真空时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("A_腔破真空时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+
+			// 4.A_腔真空到达值
+			params.push_back(CParam("A_腔真空到达值", "", this->getName().c_str(), (double)toFloat(&pszData[i])));
+			i += 4;
+
+			// 5.A_腔温控表主控温度设定
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
+			params.push_back(CParam("A_腔温控表主控温度设定", "", this->getName().c_str(), v * 0.1f));
+			i += 4;
+		}
+		else if (v == 1) {
+			// 2.B_腔加热时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("B_腔加热时间", "", this->getName().c_str(), v * 0.1f));
+			i += 2;
+
+			// 3.A_腔破真空时间
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
+			params.push_back(CParam("B_腔破真空时间", "", this->getName().c_str(), v * 0.01f));
+			i += 2;
+
+			// 4.A_腔真空到达值
+			params.push_back(CParam("B_腔真空到达值", "", this->getName().c_str(), (double)toFloat(&pszData[i])));
+			i += 4;
+
+			// 5.A_腔温控表主控温度设定
+			v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
+			params.push_back(CParam("B_腔温控表主控温度设定", "", this->getName().c_str(), v * 0.1f));
+			i += 4;
+		}
+
+		return (int)params.size();
 	}
 
 	int CVacuumBake::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
diff --git a/SourceCode/Bond/Servo/ClientListDlg.cpp b/SourceCode/Bond/Servo/ClientListDlg.cpp
new file mode 100644
index 0000000..500062e
--- /dev/null
+++ b/SourceCode/Bond/Servo/ClientListDlg.cpp
@@ -0,0 +1,141 @@
+锘�#include "stdafx.h"
+#include "ClientListDlg.h"
+#include "afxdialogex.h"
+#include "../DAQBridge/core/Collector.h"
+#include "CMaster.h"
+#include "Model.h"
+#include "Servo.h"
+#include <chrono>
+#include <iomanip>
+#include <sstream>
+
+// CClientListDlg 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CClientListDlg, CDialogEx)
+
+CClientListDlg::CClientListDlg(CWnd* pParent /*=nullptr*/)
+	: CDialogEx(IDD_DIALOG_CLIENT_LIST, pParent)
+{
+}
+
+CClientListDlg::~CClientListDlg()
+{
+}
+
+void CClientListDlg::DoDataExchange(CDataExchange* pDX)
+{
+	CDialogEx::DoDataExchange(pDX);
+	DDX_Control(pDX, IDC_LIST_CLIENTS, m_listClients);
+}
+
+BEGIN_MESSAGE_MAP(CClientListDlg, CDialogEx)
+	ON_BN_CLICKED(IDC_BUTTON_REFRESH, &CClientListDlg::OnBnClickedRefresh)
+	ON_WM_SYSCOMMAND()
+END_MESSAGE_MAP()
+
+// CClientListDlg 娑堟伅澶勭悊绋嬪簭
+
+BOOL CClientListDlg::OnInitDialog()
+{
+	CDialogEx::OnInitDialog();
+
+	// 鍒濆鍖栧垪琛ㄦ帶浠�
+	m_listClients.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_HEADERDRAGDROP);
+	
+	// 娣诲姞鍒�
+	m_listClients.InsertColumn(0, _T("IP鍦板潃"), LVCFMT_LEFT, 120);
+	m_listClients.InsertColumn(1, _T("绔彛"), LVCFMT_CENTER, 80);
+	m_listClients.InsertColumn(2, _T("鐗堟湰鐘舵��"), LVCFMT_CENTER, 100);
+	m_listClients.InsertColumn(3, _T("杩炴帴鐘舵��"), LVCFMT_CENTER, 100);
+	m_listClients.InsertColumn(4, _T("杩炴帴鏃堕棿"), LVCFMT_LEFT, 150);
+
+	// 鍒锋柊瀹㈡埛绔垪琛�
+	RefreshClientList();
+
+	return TRUE;  // 闄ら潪灏嗙劍鐐硅缃埌鎺т欢锛屽惁鍒欒繑鍥� TRUE
+}
+
+void CClientListDlg::OnBnClickedRefresh()
+{
+	RefreshClientList();
+}
+
+void CClientListDlg::OnSysCommand(UINT nID, LPARAM lParam)
+{
+	if (nID == SC_CLOSE)
+	{
+		CDialogEx::OnCancel();
+		return;
+	}
+	CDialogEx::OnSysCommand(nID, lParam);
+}
+
+void CClientListDlg::RefreshClientList()
+{
+	// 娓呯┖褰撳墠鍒楄〃
+	m_listClients.DeleteAllItems();
+	m_clients.clear();
+
+	// 鑾峰彇Collector瀹炰緥
+	extern CServoApp theApp;
+	SERVO::CMaster& master = theApp.m_model.getMaster();
+	Collector* pCollector = master.getCollector();
+	
+	if (pCollector)
+	{
+		// 鑾峰彇鐪熷疄鐨勫鎴风鍒楄〃
+		auto clientSummaries = pCollector->getClientList();
+		
+		// 杞崲鏁版嵁鏍煎紡
+		for (const auto& summary : clientSummaries)
+		{
+			ClientInfo client;
+			client.ip = summary.ip;
+			client.port = summary.port;
+			client.versionOk = summary.versionOk;
+			client.status = summary.versionOk ? "宸茶繛鎺�" : "鐗堟湰涓嶅尮閰�";
+			
+			// 鑾峰彇褰撳墠鏃堕棿浣滀负杩炴帴鏃堕棿锛堝疄闄呭疄鐜颁腑搴旇浠嶤ollector鑾峰彇鐪熷疄杩炴帴鏃堕棿锛�
+			auto now = std::chrono::system_clock::now();
+			auto time_t = std::chrono::system_clock::to_time_t(now);
+			std::tm tm;
+			localtime_s(&tm, &time_t);
+			
+			std::ostringstream oss;
+			oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
+			client.connectTime = oss.str();
+			
+			m_clients.push_back(client);
+		}
+	}
+	else
+	{
+		// 濡傛灉鏃犳硶鑾峰彇Collector瀹炰緥锛屾樉绀烘彁绀轰俊鎭�
+		ClientInfo noData;
+		noData.ip = "鏃犳硶鑾峰彇鏁版嵁";
+		noData.port = 0;
+		noData.versionOk = false;
+		noData.status = "Collector鏈垵濮嬪寲";
+		noData.connectTime = "";
+		m_clients.push_back(noData);
+	}
+
+	// 鏇存柊鍒楄〃鏄剧ず
+	UpdateClientList(m_clients);
+}
+
+void CClientListDlg::UpdateClientList(const std::vector<ClientInfo>& clients)
+{
+	m_listClients.DeleteAllItems();
+
+	for (size_t i = 0; i < clients.size(); ++i)
+	{
+		const ClientInfo& client = clients[i];
+		
+		int nItem = m_listClients.InsertItem(i, CString(client.ip.c_str()));
+		m_listClients.SetItemText(nItem, 1, CString(std::to_string(client.port).c_str()));
+		m_listClients.SetItemText(nItem, 2, client.versionOk ? _T("姝e父") : _T("寮傚父"));
+		m_listClients.SetItemText(nItem, 3, CString(client.status.c_str()));
+		m_listClients.SetItemText(nItem, 4, CString(client.connectTime.c_str()));
+	}
+}
diff --git a/SourceCode/Bond/Servo/ClientListDlg.h b/SourceCode/Bond/Servo/ClientListDlg.h
new file mode 100644
index 0000000..106ede1
--- /dev/null
+++ b/SourceCode/Bond/Servo/ClientListDlg.h
@@ -0,0 +1,47 @@
+锘�#pragma once
+#include "afxwin.h"
+#include <vector>
+#include <string>
+
+// 瀹㈡埛绔俊鎭粨鏋�
+struct ClientInfo {
+	std::string ip;
+	uint16_t port;
+	bool versionOk;
+	std::string status;
+	std::string connectTime;
+};
+
+// CClientListDlg 瀵硅瘽妗�
+
+class CClientListDlg : public CDialogEx
+{
+	DECLARE_DYNAMIC(CClientListDlg)
+
+public:
+	CClientListDlg(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CClientListDlg();
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_DIALOG_CLIENT_LIST };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnBnClickedRefresh();
+	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
+
+private:
+	void RefreshClientList();
+	void UpdateClientList(const std::vector<ClientInfo>& clients);
+
+private:
+	CListCtrl m_listClients;
+	std::vector<ClientInfo> m_clients;
+};
diff --git a/SourceCode/Bond/Servo/Configuration.cpp b/SourceCode/Bond/Servo/Configuration.cpp
index 8b8d49a..a2434b1 100644
--- a/SourceCode/Bond/Servo/Configuration.cpp
+++ b/SourceCode/Bond/Servo/Configuration.cpp
@@ -136,6 +136,16 @@
 	return TRUE;
 }
 
+BOOL CConfiguration::setPortType(unsigned int index, int type)
+{
+	if (index >= 4) return FALSE;
+	if (type < 1 || 7 < type) return FALSE;
+
+	static char* pszSection[] = { "Port1", "Port2", "Port3", "Port4" };
+	WritePrivateProfileString(pszSection[index], _T("Type"), std::to_string(type).c_str(), m_strFilepath);
+	return true;
+}
+
 BOOL CConfiguration::setPortCassetteType(unsigned int index, int cassetteType)
 {
 	if (index >= 4) return FALSE;
diff --git a/SourceCode/Bond/Servo/Configuration.h b/SourceCode/Bond/Servo/Configuration.h
index ea74b6a..0bf2628 100644
--- a/SourceCode/Bond/Servo/Configuration.h
+++ b/SourceCode/Bond/Servo/Configuration.h
@@ -24,6 +24,7 @@
 	int getFilterMode();
 	BOOL getPortParms(unsigned int index, BOOL& bEnable, int& type, int& mode,
 		int& cassetteType, int& transferMode, BOOL& bAutoChangeEnable);
+	BOOL setPortType(unsigned int index, int type);
 	BOOL setPortCassetteType(unsigned int index, int cassetteType);
 	BOOL setPortEnable(unsigned int index, BOOL bEnable);
 	BOOL isCompareMapsBeforeProceeding();
diff --git a/SourceCode/Bond/Servo/GlassJson.cpp b/SourceCode/Bond/Servo/GlassJson.cpp
index 91e7bed..5cbdd6d 100644
--- a/SourceCode/Bond/Servo/GlassJson.cpp
+++ b/SourceCode/Bond/Servo/GlassJson.cpp
@@ -190,7 +190,38 @@
         root["path"] = std::move(arr);
     }
 
-    root["payload_version"] = 1;
+    // SV数据:三层结构序列化
+    {
+        Json::Value svDataObj(Json::objectValue);
+        const auto& allSVData = g.getAllSVData();
+
+        for (const auto& machinePair : allSVData) {
+            int machineId = machinePair.first;
+            const auto& dataTypes = machinePair.second;
+
+            Json::Value machineObj(Json::objectValue);
+            for (const auto& dataTypePair : dataTypes) {
+                const std::string& dataType = dataTypePair.first;
+                const auto& dataItems = dataTypePair.second;
+
+                Json::Value dataArray(Json::arrayValue);
+                for (const auto& item : dataItems) {
+                    Json::Value itemObj(Json::objectValue);
+                    // 时间戳转换为毫秒字符串
+                    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+                        item.timestamp.time_since_epoch()).count();
+                    itemObj["t"] = std::to_string(ms);
+                    itemObj["v"] = item.value;
+                    dataArray.append(itemObj);
+                }
+                machineObj[dataType] = dataArray;
+            }
+            svDataObj[std::to_string(machineId)] = machineObj;
+        }
+        root["sv_datas"] = svDataObj;
+    }
+
+    root["payload_version"] = 2; // 版本升级,因为新增了sv_datas字段
     return root;
 }
 
@@ -308,6 +339,36 @@
             if (JBool(n, "processed", false)) tail->processEnd();
         }
     }
+
+    // SV数据:反序列化三层结构
+    if (root.isMember("sv_datas") && root["sv_datas"].isObject()) {
+        g.clearAllSVData(); // 清空现有数据
+
+        const auto& svDataObj = root["sv_datas"];
+        auto memberNames = svDataObj.getMemberNames();
+
+        for (const auto& machineIdStr : memberNames) {
+            int machineId = std::stoi(machineIdStr);
+            const auto& machineObj = svDataObj[machineIdStr];
+
+            auto dataTypeNames = machineObj.getMemberNames();
+            for (const auto& dataType : dataTypeNames) {
+                const auto& dataArray = machineObj[dataType];
+                if (dataArray.isArray()) {
+                    for (const auto& itemObj : dataArray) {
+                        long long timestampMs = 0;
+                        double value = 0.0;
+
+                        if (get_ll_from_json(itemObj, "t", timestampMs)) {
+                            value = JDouble(itemObj, "v", 0.0);
+                            // 添加SV数据
+                            g.addSVData(machineId, dataType, timestampMs, value);
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
 
 // ==================== 便捷封装 ====================
@@ -333,4 +394,4 @@
     }
     FromJson(j, g);
     return true;
-}
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/GlassLogDb.cpp b/SourceCode/Bond/Servo/GlassLogDb.cpp
index b9f0df5..d348bd9 100644
--- a/SourceCode/Bond/Servo/GlassLogDb.cpp
+++ b/SourceCode/Bond/Servo/GlassLogDb.cpp
@@ -524,3 +524,55 @@
     ofs.flush();
     return rows;
 }
+
+std::optional<GlassLogDb::Row> GlassLogDb::queryById(long long id) {
+    std::lock_guard<std::mutex> lk(mtx_);
+
+    const char* sql =
+        "SELECT id, cassette_seq_no, job_seq_no, class_id, material_type, state,"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_start, 'localtime'), ''),"
+        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_end,   'localtime'), ''),"
+        " buddy_id, aoi_result, path, params, pretty"
+        " FROM " GLASS_LOG_TABLE
+        " WHERE id = ?";
+
+    sqlite3_stmt* stmt = nullptr;
+    try {
+        throwIf(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), db_, "prepare queryById");
+        throwIf(sqlite3_bind_int64(stmt, 1, id), db_, "bind id");
+
+        std::optional<Row> result;
+
+        int rc = sqlite3_step(stmt);
+        if (rc == SQLITE_ROW) {
+            Row row;
+            row.id = sqlite3_column_int64(stmt, 0);
+            row.cassetteSeqNo = sqlite3_column_int(stmt, 1);
+            row.jobSeqNo = sqlite3_column_int(stmt, 2);
+            row.classId = safe_text(stmt, 3);
+            row.materialType = sqlite3_column_int(stmt, 4);
+            row.state = sqlite3_column_int(stmt, 5);
+            row.tStart = safe_text(stmt, 6);
+            row.tEnd = safe_text(stmt, 7);
+            row.buddyId = safe_text(stmt, 8);
+            row.aoiResult = sqlite3_column_int(stmt, 9);
+            row.path = safe_text(stmt, 10);
+            row.params = safe_text(stmt, 11);
+            row.pretty = safe_text(stmt, 12);
+
+            result = std::move(row);
+        }
+        else if (rc != SQLITE_DONE) {
+            throwIf(rc, db_, "queryById step");
+        }
+
+        sqlite3_finalize(stmt);
+        return result;
+    }
+    catch (const std::exception& e) {
+        if (stmt) sqlite3_finalize(stmt);
+        TRACE("GlassLogDb::queryById exception: %s\n", e.what());
+        return std::nullopt;
+    }
+}
+
diff --git a/SourceCode/Bond/Servo/GlassLogDb.h b/SourceCode/Bond/Servo/GlassLogDb.h
index 2bdf9bc..962db6a 100644
--- a/SourceCode/Bond/Servo/GlassLogDb.h
+++ b/SourceCode/Bond/Servo/GlassLogDb.h
@@ -96,6 +96,9 @@
         int limit = 50,
         int offset = 0);
 
+    // 按 ID 查询单条记录(包含 pretty 字段)
+    std::optional<Row> queryById(long long id);
+
     // 统计与 filters 相同条件的总数
     long long count(const Filters& filters = {});
 
diff --git a/SourceCode/Bond/Servo/GroupLabel.cpp b/SourceCode/Bond/Servo/GroupLabel.cpp
new file mode 100644
index 0000000..57f31d0
--- /dev/null
+++ b/SourceCode/Bond/Servo/GroupLabel.cpp
@@ -0,0 +1,275 @@
+#include "stdafx.h"
+#include "GroupLabel.h"
+
+
+CGroupLabel::CGroupLabel()
+{
+	m_crBkgnd = RGB(255, 255, 255);
+	m_crLine = RGB(188, 188, 188);
+	m_crText = RGB(88, 88, 88);
+	m_nPadding[PADDING_LEFT] = 5;
+	m_nPadding[PADDING_TOP] = 5;
+	m_nPadding[PADDING_RIGHT] = 5;
+	m_nPadding[PADDING_BOTTOM] = 5;
+	m_nTextAlign = TEXT_ALIGN_CENTER;
+	m_bEnableResize = FALSE;
+}
+
+
+CGroupLabel::~CGroupLabel()
+{
+}
+BEGIN_MESSAGE_MAP(CGroupLabel, CStatic)
+	ON_WM_PAINT()
+	ON_WM_LBUTTONDOWN()
+	ON_WM_SETCURSOR()
+END_MESSAGE_MAP()
+
+
+void CGroupLabel::EnableResize()
+{
+	m_bEnableResize = TRUE;
+
+	DWORD dwStyle = GetStyle();
+	dwStyle |= SS_NOTIFY;
+	SetWindowLong(GetSafeHwnd(), GWL_STYLE, dwStyle);
+}
+
+void CGroupLabel::DisableResize()
+{
+	m_bEnableResize = FALSE;
+}
+
+void CGroupLabel::OnPaint()
+{
+	CPaintDC dc(this); // device context for painting
+					   // TODO: 在此处添加消息处理程序代码
+					   // 不为绘图消息调用 CStatic::OnPaint()
+
+
+	HDC hMemDC;
+	HBITMAP hBitmap;
+	RECT rcClient;
+	CString strText;
+	HFONT hFont;
+	HBRUSH hBrushBK;
+
+			
+	::GetClientRect(m_hWnd, &rcClient);
+	hMemDC = ::CreateCompatibleDC(dc.m_hDC);
+	hBitmap = ::CreateCompatibleBitmap(dc.m_hDC, rcClient.right - rcClient.left,
+		rcClient.bottom - rcClient.top);
+	::SelectObject(hMemDC, hBitmap);
+	::SetBkMode(hMemDC, TRANSPARENT);
+
+
+	// 背景颜色
+	hBrushBK = CreateSolidBrush(m_crBkgnd);
+	::FillRect(hMemDC, &rcClient, hBrushBK);
+	DeleteObject(hBrushBK);
+
+
+	// 窗口标题
+	int x1, x2, x3, x4;
+	x1 = rcClient.left + m_nPadding[PADDING_LEFT];
+	x4 = rcClient.right - m_nPadding[PADDING_RIGHT];
+	hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
+	::SelectObject(hMemDC, hFont);
+	::SetTextColor(hMemDC, m_crText);
+	char szTitle[256];
+	int nTextLen = ::GetWindowText(m_hWnd, szTitle, 256);
+	if (nTextLen > 0) {
+		RECT rcTitle;
+		SIZE sizeText;
+
+		rcTitle.left = x1;
+		rcTitle.top = rcClient.top + 2;
+		rcTitle.bottom = rcClient.bottom - 2;
+		rcTitle.right = x4;
+
+#define TEXT_SPAGE	12
+		GetTextExtentPoint32(hMemDC, szTitle, nTextLen, &sizeText);
+		if (TEXT_ALIGN_LEFT == m_nTextAlign) {
+			::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
+			x1 += sizeText.cx + TEXT_SPAGE;
+		}
+		else if (TEXT_ALIGN_CENTER == m_nTextAlign) {
+			::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
+			x2 = x1 + (x4 - x1 - sizeText.cx - TEXT_SPAGE * 2) / 2;
+			x3 = x2 + sizeText.cx + TEXT_SPAGE * 2;
+		}
+		else {
+			::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
+			x4 -= (sizeText.cx + TEXT_SPAGE);
+		}
+
+
+	}
+
+
+	// 分隔线
+	HPEN hPen = CreatePen(PS_SOLID, 1, m_crLine);
+	HPEN hOldPen = (HPEN)SelectObject(hMemDC, hPen);
+	int y = (rcClient.bottom - rcClient.top) / 2;
+
+	if (TEXT_ALIGN_LEFT == m_nTextAlign) {
+		::MoveToEx(hMemDC, x1, y, NULL);
+		::LineTo(hMemDC, x4, y);
+	}
+	else if (TEXT_ALIGN_CENTER == m_nTextAlign) {
+		::MoveToEx(hMemDC, x1, y, NULL);
+		::LineTo(hMemDC, x2, y);
+		::MoveToEx(hMemDC, x3, y, NULL);
+		::LineTo(hMemDC, x4, y);
+	}
+	else {
+		::MoveToEx(hMemDC, x1, y, NULL);
+		::LineTo(hMemDC, x4, y);
+	}
+
+	SelectObject(hMemDC, hOldPen);
+	::DeleteObject(hPen);
+
+
+	// EndPaint
+	::BitBlt(dc.m_hDC, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top,
+		hMemDC, 0, 0, SRCCOPY);
+	::DeleteObject(hBitmap);
+	::DeleteDC(hMemDC);
+}
+
+void CGroupLabel::SetTextAlignMode(int nAlign)
+{
+	if (nAlign == TEXT_ALIGN_LEFT || nAlign == TEXT_ALIGN_CENTER || nAlign == TEXT_ALIGN_RIGHT) {
+		m_nTextAlign = nAlign;
+
+		RECT rcClient;
+		::GetClientRect(m_hWnd, &rcClient);
+		::InvalidateRect(m_hWnd, &rcClient, TRUE);
+	}
+}
+
+void CGroupLabel::Setpadding(int type, unsigned int nPadding)
+{
+	if (type >= PADDING_LEFT && PADDING_LEFT <= PADDING_BOTTOM) {
+		m_nPadding[type] = nPadding;
+	}
+}
+
+/*
+ * 设置背景色
+ * color -- 背景色
+ */
+void CGroupLabel::SetBkgndColor(COLORREF color)
+{
+	m_crBkgnd = color;
+}
+
+/*
+ * 设置文本色
+ * color -- 背景色
+ */
+void CGroupLabel::SetTextColor(COLORREF color)
+{
+	m_crText = color;
+
+	RECT rcClient;
+	::GetClientRect(m_hWnd, &rcClient);
+	::InvalidateRect(m_hWnd, &rcClient, TRUE);
+}
+
+/*
+ * 设置线色
+ * color -- 背景色
+ */
+void CGroupLabel::SetLineColor(COLORREF color)
+{
+	m_crLine = color;
+
+	RECT rcClient;
+	::GetClientRect(m_hWnd, &rcClient);
+	::InvalidateRect(m_hWnd, &rcClient, TRUE);
+}
+
+ void CGroupLabel::OnLButtonDown(UINT nFlags, CPoint point)
+ {
+	 if (!m_bEnableResize) {
+		 CStatic::OnLButtonDown(nFlags, point);
+		 return;
+	 }
+
+	 CPoint pt, ptNew;
+	 pt = point;
+	 int nMoveY = 0;
+
+	 // 捕捉鼠标消息,检测是否拖动
+	 CRect rcParent, rcWindows;
+	 GetClientRect(&rcWindows);
+	 ::ClientToScreen(m_hWnd, (LPPOINT)&rcWindows);
+	 ::ClientToScreen(m_hWnd, (LPPOINT)&rcWindows.right);
+	 GetParent()->GetClientRect(&rcParent);
+	 ::ClientToScreen(GetParent()->m_hWnd, (LPPOINT)&rcParent);
+	 HDC hDC = ::GetDC(::GetDesktopWindow());
+	 ::DrawFocusRect(hDC, &rcWindows);
+
+	 if (::GetCapture() == NULL) {
+		 SetCapture();
+		 ASSERT(this == GetCapture());
+		 AfxLockTempMaps();
+		 for (;;) {
+			 MSG msg;
+			 VERIFY(::GetMessage(&msg, NULL, 0, 0));
+
+			 if (GetCapture() != this) break;
+
+			 switch (msg.message)
+			 {
+			 case WM_MOUSEMOVE:
+				 ptNew = msg.pt;
+				 if (ptNew.y < rcParent.top) ptNew.y = rcParent.top;
+				 ::DrawFocusRect(hDC, &rcWindows);
+				 rcWindows.top = ptNew.y - 3;
+				 rcWindows.bottom = ptNew.y + 3;
+				 ::DrawFocusRect(hDC, &rcWindows);
+				 ::ScreenToClient(m_hWnd, &ptNew);
+				 break;
+
+			 case WM_LBUTTONUP:
+				 ptNew = msg.pt;
+				 ::ScreenToClient(m_hWnd, &ptNew);
+				 nMoveY = ptNew.y - pt.y;
+				 goto ExitLoop;
+
+			 case WM_KEYDOWN:
+				 if (msg.wParam == VK_ESCAPE) {
+					 goto ExitLoop;
+				 }
+				 break;
+
+			 default:
+				 DispatchMessage(&msg);
+				 break;
+			 }
+		 }
+
+	 ExitLoop:
+		 ::DrawFocusRect(hDC, &rcWindows);
+		 ::ReleaseDC(::GetDesktopWindow(), hDC);
+		 ReleaseCapture();
+		 ::InvalidateRect(m_hWnd, NULL, TRUE);
+		 GetParent()->SendMessage(ID_MSG_RESIZEY, nMoveY, 0);
+		 AfxUnlockTempMaps(FALSE);
+	 }
+
+	 CStatic::OnLButtonDown(nFlags, point);
+ }
+
+ BOOL CGroupLabel::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
+ {
+	 if (m_bEnableResize) {
+		 ::SetCursor(::LoadCursor(NULL, IDC_SIZENS));
+		 return TRUE;
+	 }
+
+	 return CStatic::OnSetCursor(pWnd, nHitTest, message);
+ }
diff --git a/SourceCode/Bond/Servo/GroupLabel.h b/SourceCode/Bond/Servo/GroupLabel.h
new file mode 100644
index 0000000..ba259f6
--- /dev/null
+++ b/SourceCode/Bond/Servo/GroupLabel.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#define PADDING_LEFT		0
+#define PADDING_TOP			1
+#define PADDING_RIGHT		2
+#define PADDING_BOTTOM		3
+
+#define TEXT_ALIGN_LEFT		0
+#define TEXT_ALIGN_CENTER	1
+#define TEXT_ALIGN_RIGHT	3
+
+#define ID_MSG_RESIZEY		WM_USER+123
+
+class CGroupLabel : public CStatic
+{
+public:
+	CGroupLabel();
+	~CGroupLabel();
+
+
+public:
+	void SetBkgndColor(COLORREF color);
+	void SetTextColor(COLORREF color);
+	void SetLineColor(COLORREF color);
+	void Setpadding(int type, unsigned int nPadding);
+	void SetTextAlignMode(int nAlign);
+	void EnableResize();
+	void DisableResize();
+
+private:
+	COLORREF	m_crBkgnd;
+	unsigned int m_nPadding[4];
+	COLORREF	m_crLine;
+	COLORREF	m_crText;
+	int			m_nTextAlign;
+
+private:
+	BOOL m_bEnableResize;
+
+	DECLARE_MESSAGE_MAP()
+	afx_msg void OnPaint();
+public:
+	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
+	afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
+};
+
diff --git a/SourceCode/Bond/Servo/Model.cpp b/SourceCode/Bond/Servo/Model.cpp
index 8d987b6..8530738 100644
--- a/SourceCode/Bond/Servo/Model.cpp
+++ b/SourceCode/Bond/Servo/Model.cpp
@@ -49,12 +49,18 @@
 	for (int i = 0; i < 4; i++) {
 		m_configuration.getPortParms(i, portEnable, portType, portMode,
 			cassetteType, transferMode, autoChangeEnable);
-		m_master.setPortType(i, portEnable, portType, portMode, cassetteType,
+		m_master.setPortTypeEx(i, portEnable, portType, portMode, cassetteType,
 			transferMode, autoChangeEnable);
 
 		int seed = m_configuration.getPortCassetteSnSeed(i + 1);
 		m_master.setPortCassetteSnSeed(i + 1, seed);
 	}
+}
+
+void CModel::setPortType(unsigned int index, SERVO::PortType type)
+{
+	m_master.setPortType(index, (int)type);
+	m_configuration.setPortType(index, (int)type);
 }
 
 void CModel::setPortCassetteType(unsigned int index, SERVO::CassetteType type)
@@ -468,9 +474,8 @@
 	// 加截Job
 	strMasterDataFile.Format(_T("%s\\MasterState.dat"), (LPTSTR)(LPCTSTR)m_strWorkDir);
 	std::string strPath = std::string((LPTSTR)(LPCTSTR)strMasterDataFile);
-	if (!m_master.loadState(strPath)) {
-		LOGE("<Master>加载MasterState.dat文件失败.");
-	}
+	m_master.setStateFile(strPath);
+
 
 
 	// 加载警告信息
diff --git a/SourceCode/Bond/Servo/Model.h b/SourceCode/Bond/Servo/Model.h
index 98f5e49..e1b1563 100644
--- a/SourceCode/Bond/Servo/Model.h
+++ b/SourceCode/Bond/Servo/Model.h
@@ -15,6 +15,7 @@
 	SERVO::CMaster& getMaster();
 	void setWorkDir(const char* pszWorkDir);
 	void loadPortParams();
+	void setPortType(unsigned int index, SERVO::PortType type);;
 	void setPortCassetteType(unsigned int index, SERVO::CassetteType type);
 	void setPortEnable(unsigned int index, BOOL bEnable);
 	int init();
diff --git a/SourceCode/Bond/Servo/PageLog.cpp b/SourceCode/Bond/Servo/PageLog.cpp
index 98bcfae..555f862 100644
--- a/SourceCode/Bond/Servo/PageLog.cpp
+++ b/SourceCode/Bond/Servo/PageLog.cpp
@@ -84,7 +84,7 @@
 										std::regex((LPTSTR)(LPCTSTR)m_strFilterText));
 								}
 								catch (const std::regex_error& e) {
-
+									TRACE(_T("正在表达式匹配检测异常: %s\n"), e.what());
 								}
 							}
 							if (m_filterMode == FilterMode::Exclude) {
diff --git a/SourceCode/Bond/Servo/ProcessJob.cpp b/SourceCode/Bond/Servo/ProcessJob.cpp
index b672e27..184b298 100644
--- a/SourceCode/Bond/Servo/ProcessJob.cpp
+++ b/SourceCode/Bond/Servo/ProcessJob.cpp
@@ -7,9 +7,12 @@
 
 namespace SERVO {
     static inline std::string trimCopy(std::string s) {
-        auto notspace = [](int ch) { return !std::isspace(ch); };
-        s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
-        s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+        s.erase(s.begin(),
+            std::find_if(s.begin(), s.end(),
+                [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }));
+        s.erase(std::find_if(s.rbegin(), s.rend(),
+            [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }).base(),
+            s.end());
         return s;
     }
 
@@ -21,6 +24,12 @@
     CProcessJob::CProcessJob(std::string pjId)
         : m_pjId(trimCopy(pjId))
     {
+        clampString(m_pjId, MAX_ID_LEN);
+    }
+
+    void CProcessJob::setId(std::string& id)
+    {
+        m_pjId = trimCopy(id);
         clampString(m_pjId, MAX_ID_LEN);
     }
 
@@ -61,7 +70,7 @@
         m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
     }
 
-    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issues()
+    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issues() const
     {
         return m_issues;
     }
@@ -177,9 +186,11 @@
         return true;
     }
 
-    bool CProcessJob::abort() {
+    bool CProcessJob::abort(std::string reason) {
         if (m_state == PJState::Completed || m_state == PJState::Aborted || m_state == PJState::Failed)
             return false;
+        m_failReason = trimCopy(reason);
+        clampString(m_failReason, 128);
         m_state = PJState::Aborted;
         markEnd();
         return true;
@@ -266,6 +277,31 @@
         }
     }
 
+    bool CProcessJob::setCarrierSlotsAndContexts(std::string carrierId, std::vector<uint8_t> slots, std::vector<void*> contexts)
+    {
+        for (auto& c : m_carriers) {
+            if (c.carrierId.compare(carrierId) == 0) {
+                c.slots = std::move(slots);
+                c.contexts = std::move(contexts);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    bool CProcessJob::setCarrierContexts(std::string carrierId, std::vector<void*> contexts)
+    {
+        for (auto& c : m_carriers) {
+            if (c.carrierId.compare(carrierId) == 0) {
+                c.contexts = std::move(contexts);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     // --------- 核心:serialize/deserialize ---------
     void CProcessJob::serialize(std::ostream& os) const {
         // 头
@@ -278,7 +314,7 @@
 
         // 配方
         uint8_t recipeType = static_cast<uint8_t>(m_recipeMethod);
-        write_pod(os, m_recipeMethod);
+        write_pod(os, recipeType);
         write_string(os, m_recipeSpec);
 
         // 物料(多 Carrier & Slot)
@@ -399,13 +435,13 @@
             return "InProcess";
             break;
         case SERVO::PJState::Paused:
-            return "Queued";
+            return "Paused";
             break;
         case SERVO::PJState::Aborting:
             return "Aborting";
             break;
         case SERVO::PJState::Completed:
-            return "Queued";
+            return "Completed";
             break;
         case SERVO::PJState::Aborted:
             return "Aborted";
@@ -420,7 +456,7 @@
         return "";
     }
 
-    CarrierSlotInfo* CProcessJob::getCarrier(std::string& strId)
+    CarrierSlotInfo* CProcessJob::getCarrier(const  std::string& strId)
     {
         for (auto& item : m_carriers) {
             if (item.carrierId.compare(strId) == 0) {
@@ -430,4 +466,44 @@
 
         return nullptr;
     }
+
+    void CProcessJob::setLotId(std::string strLotId)
+    {
+        m_strLotId = strLotId;
+    }
+
+    std::string& CProcessJob::getLotId()
+    {
+        return m_strLotId;
+    }
+
+    void CProcessJob::setProductId(std::string strProductId)
+    {
+        m_strProductId = strProductId;
+    }
+
+    std::string& CProcessJob::getProductId()
+    {
+        return m_strProductId;
+    }
+
+    void CProcessJob::setOperationId(std::string strOperationId)
+    {
+        m_strOperationId = strOperationId;
+    }
+
+    std::string& CProcessJob::getOperationId()
+    {
+        return m_strOperationId;
+    }
+
+    void CProcessJob::setPjWarp(PJWarp pjWarp)
+    {
+        m_pjWarp = pjWarp;
+    }
+
+    PJWarp& CProcessJob::getPjWarp()
+    {
+        return m_pjWarp;
+    }
 }
diff --git a/SourceCode/Bond/Servo/ProcessJob.h b/SourceCode/Bond/Servo/ProcessJob.h
index 1300b46..bbc8926 100644
--- a/SourceCode/Bond/Servo/ProcessJob.h
+++ b/SourceCode/Bond/Servo/ProcessJob.h
@@ -8,6 +8,16 @@
 #include <chrono>
 #include <optional>
 
+
+
+struct PJWarp {
+    BOOL addToCj;
+    void* pj;
+    int port;
+    BOOL checkSlot[8];
+    int material[8];
+};
+
 namespace SERVO {
     /// PJ 生命周期(贴近 E40 常见状态)
     enum class PJState : uint8_t {
@@ -90,6 +100,7 @@
         explicit CProcessJob(std::string pjId);
 
         const std::string& id() const noexcept { return m_pjId; }
+        void setId(std::string& id);
         const std::string& parentCjId() const noexcept { return m_parentCjId; }
         PJState state() const noexcept { return m_state; }
         StartPolicy startPolicy() const noexcept { return m_startPolicy; }
@@ -121,7 +132,7 @@
         };
         // 返回问题清单(空=通过)
         bool validate(const IResourceView& rv);
-        const std::vector<ValidationIssue>& issues();
+        const std::vector<ValidationIssue>& issues() const;
 
         // —— 状态机(带守卫)——
         bool queue();           // NoState -> Queued
@@ -130,7 +141,7 @@
         bool pause();           // InProcess -> Paused
         bool resume();          // Paused -> InProcess
         bool complete();        // InProcess -> Completed
-        bool abort();           // Any (未终态) -> Aborted
+        bool abort(std::string reason);           // Any (未终态) -> Aborted
         bool fail(std::string reason); // 任意态 -> Failed(记录失败原因)
 
         // —— 访问器(用于上报/查询)——
@@ -153,9 +164,13 @@
         // 追加一个载具
         void addCarrier(std::string carrierId, std::vector<uint8_t> slots);
 
+        // 设置载具slots和contexts
+        bool setCarrierSlotsAndContexts(std::string carrierId, std::vector<uint8_t> slots, std::vector<void*> contexts);
+        bool setCarrierContexts(std::string carrierId, std::vector<void*> contexts);
+
         // 访问器
         const std::vector<CarrierSlotInfo>& carriers() const noexcept { return m_carriers; }
-        CarrierSlotInfo* getCarrier(std::string& strId);
+        CarrierSlotInfo* getCarrier(const std::string& strId);
 
         // 判定是否“按载具/卡位”方式
         bool usesCarrierSlots() const noexcept { return !m_carriers.empty(); }
@@ -210,6 +225,22 @@
 
         // 错误列表
         std::vector<ValidationIssue> m_issues;
+
+        // 新增
+        std::string m_strLotId;
+        std::string m_strProductId;
+        std::string m_strOperationId;
+        PJWarp m_pjWarp;
+
+    public:
+        void setLotId(std::string strLotId);
+        std::string& getLotId();
+        void setProductId(std::string strProductId);
+        std::string& getProductId();
+        void setOperationId(std::string strOperationId);
+        std::string& getOperationId();
+        void setPjWarp(PJWarp pjWarp);
+        PJWarp& getPjWarp();
     };
 }
 
diff --git a/SourceCode/Bond/Servo/Servo.cpp b/SourceCode/Bond/Servo/Servo.cpp
index be05f20..33f8657 100644
--- a/SourceCode/Bond/Servo/Servo.cpp
+++ b/SourceCode/Bond/Servo/Servo.cpp
@@ -16,6 +16,7 @@
 #include "EqsGraphWnd.h"
 #include "MapPosWnd.h"
 #include "HmTab.h"
+#include "CControlJobManagerDlg.h"
 
 
 // 声明全局变量,用于管理 GDI+ 初始化
@@ -226,6 +227,7 @@
 
 int CServoApp::ExitInstance()
 {
+	CControlJobManagerDlg::FreeState();
 	m_model.term();
 	HSMS_Term();
 	RX_Term();
diff --git a/SourceCode/Bond/Servo/Servo.rc b/SourceCode/Bond/Servo/Servo.rc
index f4782c3..f3f35d6 100644
--- a/SourceCode/Bond/Servo/Servo.rc
+++ b/SourceCode/Bond/Servo/Servo.rc
Binary files differ
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj b/SourceCode/Bond/Servo/Servo.vcxproj
index 3022283..2e09825 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj
+++ b/SourceCode/Bond/Servo/Servo.vcxproj
@@ -200,6 +200,19 @@
     <Text Include="ReadMe.txt" />
   </ItemGroup>
   <ItemGroup>
+    <ClInclude Include="..\DAQBridge\buffer\BufferManager.h" />
+    <ClInclude Include="..\DAQBridge\buffer\BufferRegistry.h" />
+    <ClInclude Include="..\DAQBridge\buffer\SampleBuffer.h" />
+    <ClInclude Include="..\DAQBridge\core\Collector.h" />
+    <ClInclude Include="..\DAQBridge\core\CommBase.h" />
+    <ClInclude Include="..\DAQBridge\core\ConnEvents.h" />
+    <ClInclude Include="..\DAQBridge\core\DataTypes.h" />
+    <ClInclude Include="..\DAQBridge\core\Display.h" />
+    <ClInclude Include="..\DAQBridge\DAQConfig.h" />
+    <ClInclude Include="..\DAQBridge\net\FrameAssembler.h" />
+    <ClInclude Include="..\DAQBridge\net\SocketComm.h" />
+    <ClInclude Include="..\DAQBridge\proto\Protocol.h" />
+    <ClInclude Include="..\DAQBridge\proto\ProtocolCodec.h" />
     <ClInclude Include="..\jsoncpp\include\json\autolink.h" />
     <ClInclude Include="..\jsoncpp\include\json\config.h" />
     <ClInclude Include="..\jsoncpp\include\json\features.h" />
@@ -210,14 +223,21 @@
     <ClInclude Include="..\jsoncpp\include\json\writer.h" />
     <ClInclude Include="..\jsoncpp\lib_json\json_batchallocator.h" />
     <ClInclude Include="CBaseDlg.h" />
+    <ClInclude Include="CCarrierSlotGrid.h" />
+    <ClInclude Include="CCarrierSlotSelector.h" />
+    <ClInclude Include="CCjPage2.h" />
+    <ClInclude Include="CCjPage3.h" />
+    <ClInclude Include="CCjPageBase.h" />
     <ClInclude Include="CControlJob.h" />
     <ClInclude Include="CControlJobDlg.h" />
+    <ClInclude Include="CControlJobManagerDlg.h" />
     <ClInclude Include="CCustomCheckBox.h" />
     <ClInclude Include="CCollectionEvent.h" />
     <ClInclude Include="CEquipmentPage3.h" />
     <ClInclude Include="CExpandableListCtrl.h" />
     <ClInclude Include="CGlassPool.h" />
     <ClInclude Include="ChangePasswordDlg.h" />
+    <ClInclude Include="ClientListDlg.h" />
     <ClInclude Include="CMyStatusbar.h" />
     <ClInclude Include="CPageCollectionEvent.h" />
     <ClInclude Include="CPageGlassList.h" />
@@ -225,6 +245,7 @@
     <ClInclude Include="CPageReport.h" />
     <ClInclude Include="CPageVarialbles.h" />
     <ClInclude Include="CParam.h" />
+    <ClInclude Include="CCjPage1.h" />
     <ClInclude Include="CProcessDataListDlg.h" />
     <ClInclude Include="CReport.h" />
     <ClInclude Include="CRobotCmdContainerDlg.h" />
@@ -254,6 +275,7 @@
     <ClInclude Include="GridControl\TitleTip.h" />
     <ClInclude Include="CRobotTask.h" />
     <ClInclude Include="CSlot.h" />
+    <ClInclude Include="GroupLabel.h" />
     <ClInclude Include="HorizontalLine.h" />
     <ClInclude Include="InputDialog.h" />
     <ClInclude Include="JobSlotGrid.h" />
@@ -373,6 +395,34 @@
     <ClInclude Include="VerticalLine.h" />
   </ItemGroup>
   <ItemGroup>
+    <ClCompile Include="..\DAQBridge\buffer\BufferManager.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\buffer\BufferRegistry.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\buffer\SampleBuffer.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\core\Collector.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\core\Display.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\net\SocketComm.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\proto\ProtocolCodec.cpp">
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
+      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
+    </ClCompile>
     <ClCompile Include="..\jsoncpp\lib_json\json_reader.cpp">
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -386,14 +436,21 @@
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
     </ClCompile>
     <ClCompile Include="CBaseDlg.cpp" />
+    <ClCompile Include="CCarrierSlotGrid.cpp" />
+    <ClCompile Include="CCarrierSlotSelector.cpp" />
+    <ClCompile Include="CCjPage2.cpp" />
+    <ClCompile Include="CCjPage3.cpp" />
+    <ClCompile Include="CCjPageBase.cpp" />
     <ClCompile Include="CControlJob.cpp" />
     <ClCompile Include="CControlJobDlg.cpp" />
+    <ClCompile Include="CControlJobManagerDlg.cpp" />
     <ClCompile Include="CCustomCheckBox.cpp" />
     <ClCompile Include="CCollectionEvent.cpp" />
     <ClCompile Include="CEquipmentPage3.cpp" />
     <ClCompile Include="CExpandableListCtrl.cpp" />
     <ClCompile Include="CGlassPool.cpp" />
     <ClCompile Include="ChangePasswordDlg.cpp" />
+    <ClCompile Include="ClientListDlg.cpp" />
     <ClCompile Include="CMyStatusbar.cpp" />
     <ClCompile Include="CPageCollectionEvent.cpp" />
     <ClCompile Include="CPageGlassList.cpp" />
@@ -401,6 +458,7 @@
     <ClCompile Include="CPageReport.cpp" />
     <ClCompile Include="CPageVarialbles.cpp" />
     <ClCompile Include="CParam.cpp" />
+    <ClCompile Include="CCjPage1.cpp" />
     <ClCompile Include="CProcessDataListDlg.cpp" />
     <ClCompile Include="CReport.cpp" />
     <ClCompile Include="CRobotCmdContainerDlg.cpp" />
@@ -428,6 +486,7 @@
     <ClCompile Include="GridControl\TitleTip.cpp" />
     <ClCompile Include="CRobotTask.cpp" />
     <ClCompile Include="CSlot.cpp" />
+    <ClCompile Include="GroupLabel.cpp" />
     <ClCompile Include="HorizontalLine.cpp" />
     <ClCompile Include="InputDialog.cpp" />
     <ClCompile Include="JobSlotGrid.cpp" />
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj.filters b/SourceCode/Bond/Servo/Servo.vcxproj.filters
index 796971e..8c439f7 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.filters
+++ b/SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -198,6 +198,35 @@
     <ClCompile Include="GlassLogDb.cpp" />
     <ClCompile Include="sqlite3.c" />
     <ClCompile Include="CProcessDataListDlg.cpp" />
+    <ClCompile Include="GroupLabel.cpp" />
+    <ClCompile Include="CControlJobManagerDlg.cpp" />
+    <ClCompile Include="CCjPage1.cpp" />
+    <ClCompile Include="CCjPage2.cpp" />
+    <ClCompile Include="CCjPage3.cpp" />
+    <ClCompile Include="CCjPageBase.cpp" />
+    <ClCompile Include="CCarrierSlotSelector.cpp" />
+    <ClCompile Include="CCarrierSlotGrid.cpp" />
+    <ClCompile Include="..\DAQBridge\buffer\BufferManager.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\buffer\BufferRegistry.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\buffer\SampleBuffer.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\core\Collector.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\core\Display.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\net\SocketComm.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
+    <ClCompile Include="..\DAQBridge\proto\ProtocolCodec.cpp">
+      <Filter>DAQBridge</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="AlarmManager.h" />
@@ -421,6 +450,53 @@
     <ClInclude Include="sqlite3.h" />
     <ClInclude Include="sqlite3ext.h" />
     <ClInclude Include="CProcessDataListDlg.h" />
+    <ClInclude Include="GroupLabel.h" />
+    <ClInclude Include="CControlJobManagerDlg.h" />
+    <ClInclude Include="CCjPage1.h" />
+    <ClInclude Include="CCjPage2.h" />
+    <ClInclude Include="CCjPage3.h" />
+    <ClInclude Include="CCjPageBase.h" />
+    <ClInclude Include="CCarrierSlotSelector.h" />
+    <ClInclude Include="CCarrierSlotGrid.h" />
+    <ClInclude Include="..\DAQBridge\buffer\BufferManager.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\buffer\BufferRegistry.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\buffer\SampleBuffer.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\core\Collector.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\core\CommBase.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\core\ConnEvents.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\core\DataTypes.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\core\Display.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\net\FrameAssembler.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\net\SocketComm.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\proto\Protocol.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\proto\ProtocolCodec.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
+    <ClInclude Include="..\DAQBridge\DAQConfig.h">
+      <Filter>DAQBridge</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="Servo.rc" />
@@ -460,5 +536,8 @@
     <Filter Include="JsonCpp">
       <UniqueIdentifier>{a877af37-2f78-484f-b1bb-276edbbcd694}</UniqueIdentifier>
     </Filter>
+    <Filter Include="DAQBridge">
+      <UniqueIdentifier>{885738f6-3122-4bb9-8308-46b7f692fb13}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj.user b/SourceCode/Bond/Servo/Servo.vcxproj.user
index 0c03257..82c7903 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.user
+++ b/SourceCode/Bond/Servo/Servo.vcxproj.user
@@ -7,6 +7,6 @@
     <RemoteDebuggerCommand>\\DESKTOP-IODBVIQ\Servo\Debug\Servo.exe</RemoteDebuggerCommand>
     <RemoteDebuggerWorkingDirectory>\\DESKTOP-IODBVIQ\Servo\Debug\</RemoteDebuggerWorkingDirectory>
     <RemoteDebuggerServerName>DESKTOP-IODBVIQ</RemoteDebuggerServerName>
-    <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
+    <DebuggerFlavor>WindowsRemoteDebugger</DebuggerFlavor>
   </PropertyGroup>
 </Project>
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/ServoDlg.cpp b/SourceCode/Bond/Servo/ServoDlg.cpp
index 74f8607..5dac166 100644
--- a/SourceCode/Bond/Servo/ServoDlg.cpp
+++ b/SourceCode/Bond/Servo/ServoDlg.cpp
@@ -1,5 +1,5 @@
-
-// ServoDlg.cpp : 实现文件
+锘�
+// ServoDlg.cpp : 瀹炵幇鏂囦欢
 //
 
 #include "stdafx.h"
@@ -28,6 +28,8 @@
 #include "CPageCollectionEvent.h"
 #include "CControlJobDlg.h"
 #include "InputDialog.h"
+#include "ClientListDlg.h"
+#include "CControlJobManagerDlg.h"
 
 
 #ifdef _DEBUG
@@ -35,32 +37,32 @@
 #endif
 
 
-/* 创建终端的定时器 */
+/* 鍒涘缓缁堢鐨勫畾鏃跺櫒 */
 #define TIMER_ID_CREATE_TERMINAL		1
 
-/* 运行时间定时器 */
+/* 杩愯鏃堕棿瀹氭椂鍣� */
 #define TIMER_ID_UPDATE_RUMTIME			2
 
 /* Test */
 #define TIMER_ID_TEST					3
 
 
-// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
+// 鐢ㄤ簬搴旂敤绋嬪簭鈥滃叧浜庘�濊彍鍗曢」鐨� CAboutDlg 瀵硅瘽妗�
 
 class CAboutDlg : public CDialogEx
 {
 public:
 	CAboutDlg();
 
-// 对话框数据
+// 瀵硅瘽妗嗘暟鎹�
 #ifdef AFX_DESIGN_TIME
 	enum { IDD = IDD_ABOUTBOX };
 #endif
 
 	protected:
-	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
 
-// 实现
+// 瀹炵幇
 protected:
 	DECLARE_MESSAGE_MAP()
 };
@@ -78,7 +80,7 @@
 END_MESSAGE_MAP()
 
 
-// CServoDlg 对话框
+// CServoDlg 瀵硅瘽妗�
 
 
 CServoDlg::CServoDlg(CWnd* pParent /*=NULL*/)
@@ -133,6 +135,8 @@
 	ON_UPDATE_COMMAND_UI(ID_MENU_TEST_MESSAGE_SET, &CServoDlg::OnUpdateMenuTestMessageSet)
 	ON_COMMAND(ID_MENU_TEST_MESSAGE_CLEAR, &CServoDlg::OnMenuTestMessageClear)
 	ON_UPDATE_COMMAND_UI(ID_MENU_TEST_MESSAGE_CLEAR, &CServoDlg::OnUpdateMenuTestMessageClear)
+	ON_COMMAND(ID_MENU_TOOLS_CLIENT_LIST, &CServoDlg::OnMenuToolsClientList)
+	ON_UPDATE_COMMAND_UI(ID_MENU_TOOLS_CLIENT_LIST, &CServoDlg::OnUpdateMenuToolsClientList)
 	ON_COMMAND(ID_MENU_HELP_ABOUT, &CServoDlg::OnMenuHelpAbout)
 	ON_WM_INITMENUPOPUP()
 	ON_WM_TIMER()
@@ -143,12 +147,12 @@
 END_MESSAGE_MAP()
 
 
-// CServoDlg 消息处理程序
+// CServoDlg 娑堟伅澶勭悊绋嬪簭
 
 void CServoDlg::InitRxWindows()
 {
 	/* code */
-	// 订阅数据
+	// 璁㈤槄鏁版嵁
 	IRxWindows* pRxWindows = RX_GetRxWindows();
 	pRxWindows->enableLog(5);
 	if (m_pObserver == NULL) {
@@ -207,7 +211,7 @@
 				else if (state == SERVO::MASTERSTATE::STARTING) {
 					m_pMyStatusbar->setBackgroundColor(STATUSBAR_BK_STARTING);
 					m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
-					m_pMyStatusbar->setRunTimeText("正在启动...");
+					m_pMyStatusbar->setRunTimeText("姝e湪鍚姩...");
 				}
 				else if (state == SERVO::MASTERSTATE::MSERROR) {
 					m_pTopToolbar->GetBtn(IDC_BUTTON_RUN)->EnableWindow(TRUE);
@@ -216,7 +220,7 @@
 					m_pTopToolbar->GetBtn(IDC_BUTTON_STOP)->EnableWindow(FALSE);
 					m_pMyStatusbar->setBackgroundColor(STATUSBAR_BK_ALARM);
 					m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
-					m_pMyStatusbar->setRunTimeText("启动失败.");
+					m_pMyStatusbar->setRunTimeText("鍚姩澶辫触.");
 					m_pTopToolbar->GetBtn(IDC_BUTTON_ALARM)->EnableWindow(TRUE);
 					KillTimer(TIMER_ID_UPDATE_RUMTIME);
 				}
@@ -229,7 +233,7 @@
 					m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
 					m_pMyStatusbar->setRunTimeText(theApp.m_model.getMaster().getLastErrorText().c_str());
 					if (theApp.m_model.getMaster().getLastError() == ER_CODE_AOI_NG) {
-						AfxMessageBox(_T("AOI检测失败,请操作员介入解决问题!"));
+						AfxMessageBox(_T("AOI妫�娴嬪け璐ワ紝璇锋搷浣滃憳浠嬪叆瑙e喅闂锛�"));
 					}
 					m_pTopToolbar->GetBtn(IDC_BUTTON_ALARM)->EnableWindow(TRUE);
 					KillTimer(TIMER_ID_UPDATE_RUMTIME);
@@ -273,13 +277,13 @@
 						}
 					}
 					else if (exCode == ROBOT_EVENT_FINISH) {
-						m_pMyStatusbar->setCurTaskBtnText("无");
+						m_pMyStatusbar->setCurTaskBtnText("鏃�");
 					}
 					else if (exCode == ROBOT_EVENT_ABORT) {
-						m_pMyStatusbar->setCurTaskBtnText("无");
+						m_pMyStatusbar->setCurTaskBtnText("鏃�");
 					}
 					else if (exCode == ROBOT_EVENT_RESTORE) {
-						m_pMyStatusbar->setCurTaskBtnText("无");
+						m_pMyStatusbar->setCurTaskBtnText("鏃�");
 					}
 				}
 			}
@@ -329,9 +333,9 @@
 {
 	CDialogEx::OnInitDialog();
 
-	// 将“关于...”菜单项添加到系统菜单中。
+	// 灏嗏�滃叧浜�...鈥濊彍鍗曢」娣诲姞鍒扮郴缁熻彍鍗曚腑銆�
 
-	// IDM_ABOUTBOX 必须在系统命令范围内。
+	// IDM_ABOUTBOX 蹇呴』鍦ㄧ郴缁熷懡浠よ寖鍥村唴銆�
 	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
 	ASSERT(IDM_ABOUTBOX < 0xF000);
 
@@ -349,17 +353,17 @@
 		}
 	}
 
-	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
-	//  执行此操作
-	SetIcon(m_hIcon, TRUE);			// 设置大图标
-	SetIcon(m_hIcon, FALSE);		// 设置小图标
+	// 璁剧疆姝ゅ璇濇鐨勫浘鏍囥��  褰撳簲鐢ㄧ▼搴忎富绐楀彛涓嶆槸瀵硅瘽妗嗘椂锛屾鏋跺皢鑷姩
+	//  鎵ц姝ゆ搷浣�
+	SetIcon(m_hIcon, TRUE);			// 璁剧疆澶у浘鏍�
+	SetIcon(m_hIcon, FALSE);		// 璁剧疆灏忓浘鏍�
 
 
 	// model init
 	theApp.m_model.init();
 	SetTimer(TIMER_ID_TEST, 1000, nullptr);
 
-	// 菜单
+	// 鑿滃崟
 	CMenu menu;
 	menu.LoadMenu(IDR_MENU_APP);
 	SetMenu(&menu);
@@ -396,13 +400,13 @@
 	CHmTab* m_pTab = CHmTab::Hook(GetDlgItem(IDC_TAB1)->m_hWnd);
 	m_pTab->SetPaddingLeft(20);
 	m_pTab->SetItemMarginLeft(18);
-	m_pTab->AddItem("状态图", FALSE);
-	m_pTab->AddItem("连接图", TRUE);
+	m_pTab->AddItem("鐘舵�佸浘", FALSE);
+	m_pTab->AddItem("杩炴帴鍥�", TRUE);
 	m_pTab->AddItem("Glass", TRUE);
-	m_pTab->AddItem("配方", TRUE);
-	m_pTab->AddItem("报警", TRUE);
-	m_pTab->AddItem("日志", TRUE);
-	m_pTab->AddItem("搬运任务", TRUE);
+	m_pTab->AddItem("閰嶆柟", TRUE);
+	m_pTab->AddItem("鎶ヨ", TRUE);
+	m_pTab->AddItem("鏃ュ織", TRUE);
+	m_pTab->AddItem("鎼繍浠诲姟", TRUE);
 	m_pTab->SetCurSel(0);
 	m_pTab->SetBkgndColor(RGB(222, 222, 222));
 	ShowChildPage(0);
@@ -424,7 +428,7 @@
 
 
 
-	// 调整初始窗口位置
+	// 璋冩暣鍒濆绐楀彛浣嶇疆
 	CRect rcWnd;
 	GetWindowRect(&rcWnd);
 	int width = GetSystemMetrics(SM_CXSCREEN);
@@ -438,22 +442,22 @@
 	Resize();
 
 
-	// 相当于延时调用master的初始化
+	// 鐩稿綋浜庡欢鏃惰皟鐢╩aster鐨勫垵濮嬪寲
 	theApp.m_model.m_master.init();
 	theApp.m_model.loadPortParams();
 
 
-	// 初始化master以后需要控件绑定数据
+	// 鍒濆鍖杕aster浠ュ悗闇�瑕佹帶浠剁粦瀹氭暟鎹�
 	m_pPageGraph1->BindEquipmentToGraph();
 
 
-	// 更新登录状态
+	// 鏇存柊鐧诲綍鐘舵��
 	UpdateLoginStatus();
-	//SystemLogManager::getInstance.log(SystemLogManager::LogType::Info, _T("BondEq启动..."));
+	//SystemLogManager::getInstance.log(SystemLogManager::LogType::Info, _T("BondEq鍚姩..."));
 	//SystemLogManager::getInstance.
 
 
-	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
+	return TRUE;  // 闄ら潪灏嗙劍鐐硅缃埌鎺т欢锛屽惁鍒欒繑鍥� TRUE
 }
 
 void CServoDlg::OnSysCommand(UINT nID, LPARAM lParam)
@@ -469,19 +473,19 @@
 	}
 }
 
-// 如果向对话框添加最小化按钮,则需要下面的代码
-//  来绘制该图标。  对于使用文档/视图模型的 MFC 应用程序,
-//  这将由框架自动完成。
+// 濡傛灉鍚戝璇濇娣诲姞鏈�灏忓寲鎸夐挳锛屽垯闇�瑕佷笅闈㈢殑浠g爜
+//  鏉ョ粯鍒惰鍥炬爣銆�  瀵逛簬浣跨敤鏂囨。/瑙嗗浘妯″瀷鐨� MFC 搴旂敤绋嬪簭锛�
+//  杩欏皢鐢辨鏋惰嚜鍔ㄥ畬鎴愩��
 
 void CServoDlg::OnPaint()
 {
 	if (IsIconic())
 	{
-		CPaintDC dc(this); // 用于绘制的设备上下文
+		CPaintDC dc(this); // 鐢ㄤ簬缁樺埗鐨勮澶囦笂涓嬫枃
 
 		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
 
-		// 使图标在工作区矩形中居中
+		// 浣垮浘鏍囧湪宸ヤ綔鍖虹煩褰腑灞呬腑
 		int cxIcon = GetSystemMetrics(SM_CXICON);
 		int cyIcon = GetSystemMetrics(SM_CYICON);
 		CRect rect;
@@ -489,7 +493,7 @@
 		int x = (rect.Width() - cxIcon + 1) / 2;
 		int y = (rect.Height() - cyIcon + 1) / 2;
 
-		// 绘制图标
+		// 缁樺埗鍥炬爣
 		dc.DrawIcon(x, y, m_hIcon);
 	}
 	else
@@ -498,8 +502,8 @@
 	}
 }
 
-//当用户拖动最小化窗口时系统调用此函数取得光标
-//显示。
+//褰撶敤鎴锋嫋鍔ㄦ渶灏忓寲绐楀彛鏃剁郴缁熻皟鐢ㄦ鍑芥暟鍙栧緱鍏夋爣
+//鏄剧ず銆�
 HCURSOR CServoDlg::OnQueryDragIcon()
 {
 	return static_cast<HCURSOR>(m_hIcon);
@@ -659,6 +663,17 @@
 	pCmdUI->Enable(TRUE);
 }
 
+void CServoDlg::OnMenuToolsClientList()
+{
+	CClientListDlg dlg;
+	dlg.DoModal();
+}
+
+void CServoDlg::OnUpdateMenuToolsClientList(CCmdUI* pCmdUI)
+{
+	pCmdUI->Enable(TRUE);
+}
+
 void CServoDlg::OnMenuHelpAbout()
 {
 	theApp.m_model.getMaster().test();
@@ -668,14 +683,14 @@
 
 void CServoDlg::OnBnClickedOk()
 {
-	// TODO: 在此添加控件通知处理程序代码
+	// TODO: 鍦ㄦ娣诲姞鎺т欢閫氱煡澶勭悊绋嬪簭浠g爜
 	CDialogEx::OnOK();
 }
 
 
 void CServoDlg::OnBnClickedCancel()
 {
-	// TODO: 在此添加控件通知处理程序代码
+	// TODO: 鍦ㄦ娣诲姞鎺т欢閫氱煡澶勭悊绋嬪簭浠g爜
 	CDialogEx::OnCancel();
 }
 
@@ -869,7 +884,7 @@
 
 void CServoDlg::OnClose()
 {
-	// TODO: 在此添加消息处理程序代码和/或调用默认值
+	// TODO: 鍦ㄦ娣诲姞娑堟伅澶勭悊绋嬪簭浠g爜鍜�/鎴栬皟鐢ㄩ粯璁ゅ��
 
 	CDialogEx::OnClose();
 }
@@ -894,7 +909,7 @@
 void CServoDlg::OnTimer(UINT_PTR nIDEvent)
 {
 	if (TIMER_ID_CREATE_TERMINAL == nIDEvent) {
-		// 预先创建终端窗口
+		// 棰勫厛鍒涘缓缁堢绐楀彛
 		KillTimer(TIMER_ID_CREATE_TERMINAL);
 		char szBuffer[MAX_PATH];
 		sprintf_s(szBuffer, MAX_PATH, "%s\\RES\\TeminalMsg.html", (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
@@ -981,7 +996,7 @@
 		::EnableMenuItem(hMenu, ID_OPEATOR_SWITCH, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
 		::EnableMenuItem(hMenu, ID_OPERATOR_LOGOUT, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
 
-		m_pTopToolbar->SetOperatorBtnText(_T("未登录"));
+		m_pTopToolbar->SetOperatorBtnText(_T("鏈櫥褰�"));
 	}
 }
 
@@ -989,30 +1004,16 @@
 {
 	int id = (int)lParam;
 	if (id == IDC_BUTTON_RUN || id == IDC_BUTTON_STOP) {
-		//CInputDialog inputDialog(_T("验证用户"), _T("请输入用户密码:"));
-		//if (inputDialog.DoModal() != IDOK) {
-		//	AfxMessageBox(_T("取消验证!"));
-		//	return 0;
-		//}
-	
-		//CString inputText = inputDialog.GetInputText();
-		//std::string strPass = UserManager::getInstance().getCurrentPass();
-		//if (inputText.Compare(strPass.c_str()) != 0) {
-		//	AfxMessageBox(_T("密码错误!"));
-		//	SystemLogManager::getInstance().log(SystemLogManager::LogType::Info, _T("验证时,密码错误!"));
-		//	return 0;
-		//}
-		
 		UserRole emRole = UserManager::getInstance().getCurrentUserRole();
 		if (emRole != UserRole::SuperAdmin) {
-			AfxMessageBox(_T("当前用户并非管理员!!!")); 
+			AfxMessageBox(_T("褰撳墠鐢ㄦ埛骞堕潪绠$悊鍛橈紒锛侊紒")); 
 			return 1;
 		}
 	}
 
 	if (id == IDC_BUTTON_RUN) {
 		if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
-			AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
+			AfxMessageBox("褰撳墠鏈夋満鍙板彂鐢熼敊璇紝涓嶈兘鍚姩锛岃纭瑙e喅闂鍚庡啀灏濊瘯閲嶆柊鍚姩锛�");
 		}
 		else {
 			if (theApp.m_model.getMaster().start() == 0) {
@@ -1024,7 +1025,7 @@
 	}
 	else if (id == IDC_BUTTON_RUN_BATCH) {
 		if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
-			AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
+			AfxMessageBox("褰撳墠鏈夋満鍙板彂鐢熼敊璇紝涓嶈兘鍚姩锛岃纭瑙e喅闂鍚庡啀灏濊瘯閲嶆柊鍚姩锛�");
 		}
 		else {
 			if (theApp.m_model.getMaster().startBatch() == 0) {
@@ -1036,7 +1037,7 @@
 	}
 	else if (id == IDC_BUTTON_RUN_CT) {
 		if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
-			AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
+			AfxMessageBox("褰撳墠鏈夋満鍙板彂鐢熼敊璇紝涓嶈兘鍚姩锛岃纭瑙e喅闂鍚庡啀灏濊瘯閲嶆柊鍚姩锛�");
 		}
 		else {
 			if (theApp.m_model.getMaster().startContinuousTransfer() == 0) {
@@ -1053,11 +1054,14 @@
 	}
 	else if (id == IDC_BUTTON_JOBS) {
 		CControlJobDlg dlg;
-		dlg.SetControlJob(theApp.m_model.m_master.getControlJob());
 		dlg.DoModal();
 	}
 	else if (id == IDC_BUTTON_PORT_CONFIG) {
 		CPortConfigurationDlg dlg;
+		dlg.DoModal();
+	}
+	else if (id == IDC_BUTTON_CASSETTE) {
+		CControlJobManagerDlg dlg;
 		dlg.DoModal();
 	}
 	else if (id == IDC_BUTTON_ROBOT) {
@@ -1092,7 +1096,7 @@
 		else if (2 == menuId) {
 			CUserManagerDlg dlg;
 			if (dlg.DoModal() != IDOK) {
-				logManager.log(SystemLogManager::LogType::Operation, _T("用户管理的预操作被取消!"));
+				logManager.log(SystemLogManager::LogType::Operation, _T("鐢ㄦ埛绠$悊鐨勯鎿嶄綔琚彇娑堬紒"));
 			}
 		}
 		else if (3 == menuId) {
@@ -1100,14 +1104,14 @@
 			dlg.DoModal();
 		}
 		else if (4 == menuId) {
-			int ret = AfxMessageBox(_T("是否切换用户?切换用户会退出当前账号!"), MB_OKCANCEL | MB_ICONEXCLAMATION);
+			int ret = AfxMessageBox(_T("鏄惁鍒囨崲鐢ㄦ埛锛熷垏鎹㈢敤鎴蜂細閫�鍑哄綋鍓嶈处鍙凤紒"), MB_OKCANCEL | MB_ICONEXCLAMATION);
 			if (ret != IDOK) {
 				return 0;
 			}
 
-			logManager.log(SystemLogManager::LogType::Operation, _T("确认切换角色!"));
+			logManager.log(SystemLogManager::LogType::Operation, _T("纭鍒囨崲瑙掕壊锛�"));
 			if (userManager.isLoggedIn()) {
-				logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
+				logManager.log(SystemLogManager::LogType::Info, _T("閫�鍑虹櫥褰曪紒"));
 				userManager.logout();
 			}
 
@@ -1116,13 +1120,13 @@
 		}
 		else {
 			CString cstrMessage;
-			cstrMessage.Format(_T("是否退出用户 [%s]?"), userManager.getCurrentUser().c_str());
+			cstrMessage.Format(_T("鏄惁閫�鍑虹敤鎴� [%s]锛�"), userManager.getCurrentUser().c_str());
 			int ret = AfxMessageBox(_T(cstrMessage), MB_OKCANCEL | MB_ICONEXCLAMATION);
 			if (ret != IDOK) {
 				return 0;
 			}
 
-			logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
+			logManager.log(SystemLogManager::LogType::Info, _T("閫�鍑虹櫥褰曪紒"));
 			userManager.logout();
 		}
 
@@ -1166,16 +1170,16 @@
 
 	SERVO::MASTERSTATE state = theApp.m_model.getMaster().getState();
 	if (state == SERVO::MASTERSTATE::RUNNING) {
-		strText.Format(_T("正在运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
+		strText.Format(_T("姝e湪杩愯锛�%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
 	}
 	else if (state == SERVO::MASTERSTATE::RUNNING_CONTINUOUS_TRANSFER) {
-		strText.Format(_T("千传模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
+		strText.Format(_T("鍗冧紶妯″紡锛�%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
 	}
 	else if (state == SERVO::MASTERSTATE::RUNNING_BATCH) {
-		strText.Format(_T("JOB模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
+		strText.Format(_T("JOB妯″紡锛�%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
 	}
 	else {
-		strText.Format(_T("已运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
+		strText.Format(_T("宸茶繍琛岋細%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
 	}
 
 	return strText;
diff --git a/SourceCode/Bond/Servo/ServoDlg.h b/SourceCode/Bond/Servo/ServoDlg.h
index b8f6748..bdc47a4 100644
--- a/SourceCode/Bond/Servo/ServoDlg.h
+++ b/SourceCode/Bond/Servo/ServoDlg.h
@@ -1,5 +1,5 @@
-
-// ServoDlg.h : 头文件
+锘�
+// ServoDlg.h : 澶存枃浠�
 //
 
 #pragma once
@@ -20,12 +20,12 @@
 #include "CPageGlassList.h"
 
 
-// CServoDlg 对话框
+// CServoDlg 瀵硅瘽妗�
 class CServoDlg : public CDialogEx
 {
-// 构造
+// 鏋勯��
 public:
-	CServoDlg(CWnd* pParent = NULL);	// 标准构造函数
+	CServoDlg(CWnd* pParent = NULL);	// 鏍囧噯鏋勯�犲嚱鏁�
 
 
 public:
@@ -50,16 +50,16 @@
 	CPageLog*	 m_pPageLog;
 	CPageTransferLog* m_pPageTransferLog;
 
-// 对话框数据
+// 瀵硅瘽妗嗘暟鎹�
 #ifdef AFX_DESIGN_TIME
 	enum { IDD = IDD_SERVO_DIALOG };
 #endif
 
 	protected:
-	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV 支持
+	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV 鏀寔
 
 
-// 实现
+// 瀹炵幇
 protected:
 	HICON m_hIcon;
 	COLORREF m_crBkgnd;
@@ -70,7 +70,7 @@
 	CPanelAttributes* m_pPanelAttributes;
 	CMyStatusbar* m_pMyStatusbar;
 
-	// 生成的消息映射函数
+	// 鐢熸垚鐨勬秷鎭槧灏勫嚱鏁�
 	virtual BOOL OnInitDialog();
 	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
 	afx_msg void OnPaint();
@@ -98,6 +98,8 @@
 	afx_msg void OnUpdateMenuTestMessageSet(CCmdUI* pCmdUI);
 	afx_msg void OnMenuTestMessageClear();
 	afx_msg void OnUpdateMenuTestMessageClear(CCmdUI* pCmdUI);
+	afx_msg void OnMenuToolsClientList();
+	afx_msg void OnUpdateMenuToolsClientList(CCmdUI* pCmdUI);
 	afx_msg void OnMenuHelpAbout();
 	afx_msg void OnTimer(UINT_PTR nIDEvent);
 	afx_msg LRESULT OnPanelResize(WPARAM wParam, LPARAM lParam);
diff --git a/SourceCode/Bond/Servo/TerminalDisplayDlg.cpp b/SourceCode/Bond/Servo/TerminalDisplayDlg.cpp
index a7a88be..81b3e4e 100644
--- a/SourceCode/Bond/Servo/TerminalDisplayDlg.cpp
+++ b/SourceCode/Bond/Servo/TerminalDisplayDlg.cpp
@@ -104,7 +104,6 @@
 
 void CTerminalDisplayDlg::Resize()
 {
-	CWnd* pItem;
 	CRect rcClient, rcItem;
 	GetClientRect(&rcClient);
 	rcClient.top += 38;
diff --git a/SourceCode/Bond/Servo/ToolUnits.cpp b/SourceCode/Bond/Servo/ToolUnits.cpp
index cc9a1bf..6d5df0d 100644
--- a/SourceCode/Bond/Servo/ToolUnits.cpp
+++ b/SourceCode/Bond/Servo/ToolUnits.cpp
@@ -4,7 +4,9 @@
 #include <memory>
 #include <sstream>
 #include <algorithm>
-
+#include <ctime>
+#include <iomanip>
+#include <sstream>
 
 CToolUnits::CToolUnits()
 {
@@ -557,3 +559,19 @@
 	std::snprintf(out, sizeof(out), "%s.%03d", date, ms);
 	return out;
 }
+
+std::string CToolUnits::NowStrSec()
+{
+	using namespace std::chrono;
+	auto now = system_clock::now();
+	std::time_t t = system_clock::to_time_t(now);
+	std::tm tm{};
+#ifdef _WIN32
+	localtime_s(&tm, &t);   // 本地时间(Windows 线程安全)
+#else
+	localtime_r(&t, &tm);   // 本地时间(POSIX 线程安全)
+#endif
+	std::ostringstream oss;
+	oss << std::put_time(&tm, "%Y%m%d%H%M%S"); // 例:2025-09-15 08:23:07
+	return oss.str();
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/ToolUnits.h b/SourceCode/Bond/Servo/ToolUnits.h
index b5f8430..f88788d 100644
--- a/SourceCode/Bond/Servo/ToolUnits.h
+++ b/SourceCode/Bond/Servo/ToolUnits.h
@@ -59,5 +59,6 @@
 	static std::string TimePointToUtcString(const std::optional<TP>& tp,
 		const char* fmt = "%Y-%m-%d %H:%M:%S");
 	static std::string TimePointToLocalStringMs(const std::optional<TP>& tp);
+	static std::string NowStrSec();
 };
 
diff --git a/SourceCode/Bond/Servo/TopToolbar.cpp b/SourceCode/Bond/Servo/TopToolbar.cpp
index 4b7755e..47d5ee4 100644
--- a/SourceCode/Bond/Servo/TopToolbar.cpp
+++ b/SourceCode/Bond/Servo/TopToolbar.cpp
@@ -37,6 +37,7 @@
 	DDX_Control(pDX, IDC_BUTTON_ALARM, m_btnAlarm);
 	DDX_Control(pDX, IDC_BUTTON_SETTINGS, m_btnSettings);
 	DDX_Control(pDX, IDC_BUTTON_PORT_CONFIG, m_btnPortConfig);
+	DDX_Control(pDX, IDC_BUTTON_CASSETTE, m_btnCassette);
 	DDX_Control(pDX, IDC_BUTTON_ROBOT, m_btnRobot);
 	DDX_Control(pDX, IDC_BUTTON_OPERATOR, m_btnOperator);
 }
@@ -66,6 +67,7 @@
 	InitBtn(m_btnSettings, "Settings_High_32.ico", "Settings_Gray_32.ico");
 	InitBtn(m_btnRobot, "Robot_High_32.ico", "Robot_Gray_32.ico");
 	InitBtn(m_btnPortConfig, "PortConfig_High_32.ico", "PortConfig_Gray_32.ico");
+	InitBtn(m_btnCassette, "Cassette_High_32.ico", "Cassette_Gray_32.ico");
 	InitBtn(m_btnOperator, "Operator_High_32.ico", "Operator_Gray_32.ico");
 	HMENU hMenu = LoadMenu(AfxGetInstanceHandle(), MAKEINTRESOURCEA(IDR_MENU_OPEATOR));
 	m_btnOperator.SetMenu(hMenu);
@@ -168,6 +170,11 @@
 	x += BTN_WIDTH;
 	x += 2;
 
+	pItem = GetDlgItem(IDC_BUTTON_CASSETTE);
+	pItem->MoveWindow(x, y, BTN_WIDTH, nBthHeight);
+	x += BTN_WIDTH;
+	x += 2;
+
 	pItem = GetDlgItem(IDC_BUTTON_ROBOT);
 	pItem->MoveWindow(x, y, BTN_WIDTH, nBthHeight);
 	x += BTN_WIDTH;
@@ -225,6 +232,7 @@
 	case IDC_BUTTON_ALARM:
 	case IDC_BUTTON_SETTINGS:
 	case IDC_BUTTON_PORT_CONFIG:
+	case IDC_BUTTON_CASSETTE:
 	case IDC_BUTTON_ROBOT:
 		GetParent()->SendMessage(ID_MSG_TOOLBAR_BTN_CLICKED, 0, LOWORD(wParam));
 		break;
diff --git a/SourceCode/Bond/Servo/TopToolbar.h b/SourceCode/Bond/Servo/TopToolbar.h
index 7117c12..c415b5d 100644
--- a/SourceCode/Bond/Servo/TopToolbar.h
+++ b/SourceCode/Bond/Servo/TopToolbar.h
@@ -38,6 +38,7 @@
 	CBlButton m_btnAlarm;
 	CBlButton m_btnSettings;
 	CBlButton m_btnPortConfig;
+	CBlButton m_btnCassette;
 	CBlButton m_btnRobot;
 	CBlButton m_btnOperator;
 
diff --git a/SourceCode/Bond/Servo/resource.h b/SourceCode/Bond/Servo/resource.h
index bd3bb31..6c9865f 100644
--- a/SourceCode/Bond/Servo/resource.h
+++ b/SourceCode/Bond/Servo/resource.h
Binary files differ
diff --git a/SourceCode/Bond/x64/Debug/EqsGraph.ini b/SourceCode/Bond/x64/Debug/EqsGraph.ini
index 3cafc72..8a58491 100644
--- a/SourceCode/Bond/x64/Debug/EqsGraph.ini
+++ b/SourceCode/Bond/x64/Debug/EqsGraph.ini
@@ -1,6 +1,6 @@
 [LoadPort 1]
 Left=23
-Top=88
+Top=87
 [LoadPort 2]
 Left=23
 Top=437
diff --git a/SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico b/SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico
new file mode 100644
index 0000000..c8bcbba
--- /dev/null
+++ b/SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico
Binary files differ
diff --git a/SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico b/SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico
new file mode 100644
index 0000000..93a16f1
--- /dev/null
+++ b/SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico
Binary files differ

--
Gitblit v1.9.3