已添加22个文件
已修改36个文件
4776 ■■■■■ 文件已修改
.gitignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/DAQConfig.h 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/BufferManager.cpp 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/BufferManager.h 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/BufferRegistry.cpp 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/BufferRegistry.h 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/SampleBuffer.cpp 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/buffer/SampleBuffer.h 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/Collector.cpp 537 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/Collector.h 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/CommBase.h 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/ConnEvents.h 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/DataTypes.h 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/Display.cpp 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/core/Display.h 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/net/FrameAssembler.h 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/net/SocketComm.cpp 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/net/SocketComm.h 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/proto/Protocol.h 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/proto/ProtocolCodec.cpp 663 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/DAQBridge/proto/ProtocolCodec.h 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CBonder.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CBonder.h 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.cpp 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.h 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.cpp 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.h 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJob.cpp 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJob.h 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobDlg.cpp 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobDlg.h 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.cpp 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CEquipment.cpp 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CEquipment.h 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CExpandableListCtrl.cpp 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CGlass.cpp 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMaster.cpp 588 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMaster.h 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageGlassList.cpp 357 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPagePortProperty.cpp 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CParam.cpp 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Configuration.cpp 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Configuration.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Model.cpp 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Model.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProcessJob.cpp 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProcessJob.h 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.rc 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj.filters 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ServoDlg.cpp 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/TopToolbar.cpp 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/TopToolbar.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/resource.h 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/x64/Debug/EqsGraph.ini 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico 补丁 | 查看 | 原始文档 | blame | 历史
.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
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;
}
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;
}
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 };
};
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);
}
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_;
};
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;
}
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_;
};
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();
}
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
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
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
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
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;
        }
    }
    // å…¶å®ƒç±»åž‹ï¼ˆå°†æ¥æ‰©å±•)……
}
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
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_;
};
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);
}
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
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
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
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
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
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);
SourceCode/Bond/Servo/CCjPage2.cpp
@@ -8,6 +8,11 @@
#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)
@@ -38,6 +43,10 @@
    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()
@@ -77,15 +86,6 @@
    m_grid.SetPortInfo(3, _T("Port 4"), _T(""));
    // æµ‹è¯•数据
    char szBuffer[256];
    for (int port = 0; port < 4; port++) {
        for (int slot = 0; slot < 8; slot++) {
            sprintf_s(szBuffer, 256, "Gls%04d%04d", port + 1, slot + 1);
            m_grid.SetSlotGlass(port, slot, TRUE, szBuffer, CCarrierSlotGrid::MAT_G1);
        }
    }
    UpdatePjData();
@@ -112,12 +112,41 @@
    pItem->GetWindowRect(&rcItem);
    ScreenToClient(rcItem);
    int x = rcItem.left + 100 + 18;
    int y = 100;
    // è®©æŽ§ä»¶çª—口尺寸自动匹配当前列宽/行数(不出现滚动条)
    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() 
@@ -234,6 +263,37 @@
        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);
            }
        }
    }
@@ -241,7 +301,6 @@
    if (portIndex != -1) {
        for (int i = 0; i < 8; i++) {
            m_grid.SetSlotChecked(portIndex, i, ((PJWarp*)m_pContext)->checkSlot[i]);
            m_grid.SetSlotMaterialType(portIndex, i, ((PJWarp*)m_pContext)->material[i]);
        }
    }
@@ -273,6 +332,7 @@
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
@@ -293,6 +353,7 @@
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
@@ -313,6 +374,7 @@
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
@@ -333,6 +395,7 @@
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
@@ -384,3 +447,34 @@
    *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);
}
SourceCode/Bond/Servo/CCjPage2.h
@@ -5,14 +5,6 @@
struct PJWarp {
    BOOL addToCj;
    void* pj;
    int port;
    BOOL checkSlot[8];
    int material[8];
};
// CPjPage1 å¯¹è¯æ¡†
class CCjPage2 : public CCjPageBase
@@ -59,4 +51,8 @@
    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();
};
SourceCode/Bond/Servo/CCjPage3.cpp
@@ -29,6 +29,7 @@
BEGIN_MESSAGE_MAP(CCjPage3, CCjPageBase)
    ON_WM_DESTROY()
    ON_EN_CHANGE(IDC_EDIT_CJ_ID, &CCjPage3::OnEnChangeEditCjId)
END_MESSAGE_MAP()
@@ -69,5 +70,40 @@
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);
}
SourceCode/Bond/Servo/CCjPage3.h
@@ -1,5 +1,6 @@
#pragma once
#include "CCjPageBase.h"
#include "CControlJob.h"
// CPjPage1 å¯¹è¯æ¡†
@@ -15,6 +16,8 @@
protected:
    void Resize();
    virtual int OnApply();
    virtual void OnSetContext(void* pContext);
    void UpdateCjData();
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
@@ -28,4 +31,5 @@
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnDestroy();
    afx_msg void OnEnChangeEditCjId();
};
SourceCode/Bond/Servo/CControlJob.cpp
@@ -38,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;
@@ -96,6 +102,11 @@
    const std::vector<CControlJob::ValidationIssue>& CControlJob::issues()
    {
        return m_issues;
    }
    void CControlJob::clearIssues()
    {
        m_issues.clear();
    }
    bool CControlJob::validateForCreate(
@@ -209,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;
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; }
@@ -71,6 +72,7 @@
            const std::function<bool(const std::string&)>& canJoinFn
        );
        const std::vector<ValidationIssue>& CControlJob::issues();
        void clearIssues();
        // â€”— S14F9 â†’ S14F10 çš„“应用结果”模型 â€”— //
        struct CreateRequest {
@@ -97,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; }
SourceCode/Bond/Servo/CControlJobDlg.cpp
@@ -6,13 +6,35 @@
#include "CControlJobDlg.h"
#include "afxdialogex.h"
// ===== æ–°å¢žï¼šæ ‡å‡†åº“头 =====
#include <array>
#include <string>
#include <unordered_set>
#include <algorithm>
// ===== æ–°å¢žï¼šCString çš„ 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 å±‚(第一列键:GlassID;空对象用 "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);
}
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);
};
SourceCode/Bond/Servo/CControlJobManagerDlg.cpp
@@ -6,6 +6,7 @@
#include "CControlJobManagerDlg.h"
#include "afxdialogex.h"
#include "ToolUnits.h"
#include "RecipeManager.h"
bool CControlJobManagerDlg::m_bHasState = false;
@@ -76,6 +77,9 @@
        else if (1 == code) {
            if (contextType == 1) {
                UpProcessJobId((PJWarp*)pContext);
            }
            else if (contextType == 2) {
                UpControlJobId((SERVO::CControlJob*)pContext);
            }
        }
    };
@@ -306,7 +310,8 @@
            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) {
@@ -405,6 +410,20 @@
    }
}
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;
@@ -423,6 +442,14 @@
void CControlJobManagerDlg::OnBnClickedButtonBathCompletion()
{
    // å…ˆæ£€æŸ¥å½“前master
    auto& master = theApp.m_model.getMaster();
    if (!master.canCreateControlJob()) {
        AfxMessageBox("当前Master有未结批的Job, è¯·å…ˆç»“批处理");
        return;
    }
    // å…ˆåº”用
    for (int i = 0; i < 3; i++) {
        if (m_pages[i]->IsWindowVisible()) {
@@ -445,9 +472,13 @@
    }
    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);
    auto& master = theApp.m_model.getMaster();
    bool bProcessStart[] = {false, false, false, false};
    std::vector<SERVO::CProcessJob*> pjs;
    for (auto item : m_pjWarps) {
        if (!item.addToCj) continue;
@@ -461,22 +492,35 @@
        }
        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 = "Port" + std::to_string(item.port + 1);
        csi.carrierId = pPorts[item.port]->getCassetteId();
        for (int i = 0; i < 8; i++) {
            if (item.checkSlot[i]) {
                csi.slots.push_back(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());
    }
@@ -487,8 +531,122 @@
        return;
    }
    m_pControlJob->setPJs(pjs);
    m_pControlJob->clearIssues();
    int nRet = master.setProcessJobs(pjs);
    master.setControlJob(*m_pControlJob);
    // æ²¡æœ‰é—®é¢˜çš„pj要释放
    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的glass中,类型等
    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);
        }
    }
}
SourceCode/Bond/Servo/CControlJobManagerDlg.h
@@ -25,6 +25,7 @@
    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();
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);
@@ -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;
    }
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:
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;
    };
    // è®¡ç®—“当前可见的所有后代数量”(基于 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 @@
    // â€”— è‹¥ç‚¹å‡»åˆ°éœ€è¦â€œå…¨æ–‡æ˜¾ç¤ºâ€çš„列,则向父窗口发送自定义通知 â€”— //
    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);
}
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";
SourceCode/Bond/Servo/CMaster.cpp
@@ -9,6 +9,11 @@
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)
@@ -213,10 +218,9 @@
        // è¯»ç¼“存数据
        readCache();
        loadState();
        // å®šæ—¶å™¨
@@ -263,6 +267,12 @@
        }
        m_listEquipment.clear();
        if (m_pCollector != nullptr) {
            m_pCollector->stopLoop();
            delete m_pCollector;
            m_pCollector = nullptr;
        }
        return 0;
    }
@@ -806,48 +816,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()) {
@@ -856,54 +850,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++) {
@@ -911,140 +915,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) {
@@ -1504,6 +1491,8 @@
                                if (m_listener.onCjEnd != nullptr) {
                                    m_listener.onCjEnd(this, pJob);
                                }
                                completeControlJob();
                            }
                        }
                    }
@@ -1542,8 +1531,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)不一致,请检查。",
@@ -1557,17 +1558,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) {
@@ -1580,6 +1622,61 @@
            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) {
                m_pCollector->buffersPush(eqid, 1, ts, params.at(1).getDoubleValue());
                m_pCollector->buffersPush(eqid, 2, ts, params.at(2).getDoubleValue());
                m_pCollector->buffersPush(eqid, 3, ts, params.at(3).getDoubleValue());
                m_pCollector->buffersPush(eqid, 4, ts, params.at(4).getDoubleValue());
                m_pCollector->buffersPush(eqid, 5, ts, params.at(5).getDoubleValue());
                m_pCollector->buffersPush(eqid, 6, ts, params.at(6).getDoubleValue());
                m_pCollector->buffersPush(eqid, 7, ts, params.at(7).getDoubleValue());
                m_pCollector->buffersPush(eqid, 8, ts, params.at(8).getDoubleValue());
                m_pCollector->buffersPush(eqid, 9, ts, params.at(9).getDoubleValue());
                m_pCollector->buffersPush(eqid, 10, ts, params.at(10).getDoubleValue());
                m_pCollector->buffersPush(eqid, 11, ts, params.at(11).getDoubleValue());
                m_pCollector->buffersPush(eqid, 12, ts, params.at(12).getDoubleValue());
                m_pCollector->buffersPush(eqid, 13, ts, params.at(13).getDoubleValue());
                m_pCollector->buffersPush(eqid, 14, ts, params.at(14).getDoubleValue());
                m_pCollector->buffersPush(eqid, 15, ts, params.at(15).getDoubleValue());
                m_pCollector->buffersPush(eqid, 16, ts, params.at(16).getDoubleValue());
            }
            else if (eqid == EQ_ID_VACUUMBAKE) {
                m_pCollector->buffersPush(eqid, 1, ts, params.at(1).getDoubleValue());
                m_pCollector->buffersPush(eqid, 2, ts, params.at(2).getDoubleValue());
                m_pCollector->buffersPush(eqid, 3, ts, params.at(3).getDoubleValue());
                m_pCollector->buffersPush(eqid, 4, ts, params.at(4).getDoubleValue());
                m_pCollector->buffersPush(eqid, 5, ts, params.at(5).getDoubleValue());
                m_pCollector->buffersPush(eqid, 6, ts, params.at(6).getDoubleValue());
                m_pCollector->buffersPush(eqid, 7, ts, params.at(7).getDoubleValue());
                m_pCollector->buffersPush(eqid, 8, ts, params.at(10).getDoubleValue());
                m_pCollector->buffersPush(eqid, 9, ts, params.at(11).getDoubleValue());
                m_pCollector->buffersPush(eqid, 10, ts, params.at(12).getDoubleValue());
                m_pCollector->buffersPush(eqid, 11, ts, params.at(13).getDoubleValue());
                m_pCollector->buffersPush(eqid, 12, ts, params.at(14).getDoubleValue());
                m_pCollector->buffersPush(eqid, 13, ts, params.at(15).getDoubleValue());
                m_pCollector->buffersPush(eqid, 14, ts, params.at(16).getDoubleValue());
            }
            else if (eqid == EQ_ID_BAKE_COOLING) {
                m_pCollector->buffersPush(eqid, 1, ts, params.at(1).getDoubleValue());
                m_pCollector->buffersPush(eqid, 2, ts, params.at(2).getDoubleValue());
                m_pCollector->buffersPush(eqid, 3, ts, params.at(3).getDoubleValue());
                m_pCollector->buffersPush(eqid, 4, ts, params.at(4).getDoubleValue());
                m_pCollector->buffersPush(eqid, 5, ts, params.at(5).getDoubleValue());
                m_pCollector->buffersPush(eqid, 6, ts, params.at(6).getDoubleValue());
                m_pCollector->buffersPush(eqid, 7, ts, params.at(11).getDoubleValue());
                m_pCollector->buffersPush(eqid, 8, ts, params.at(12).getDoubleValue());
                m_pCollector->buffersPush(eqid, 9, ts, params.at(13).getDoubleValue());
                m_pCollector->buffersPush(eqid, 10, ts, params.at(14).getDoubleValue());
                m_pCollector->buffersPush(eqid, 11, ts, params.at(15).getDoubleValue());
                m_pCollector->buffersPush(eqid, 12, ts, params.at(16).getDoubleValue());
            }
            // ä»¥ä¸‹æ˜¯è¾“出测试
            std::string strOut;
            char szBuffer[256];
            for (auto p : params) {
@@ -1853,6 +1950,7 @@
        // æ¨¡æ‹Ÿæµ‹è¯•
        /*
        static int aaa = 0;
        aaa++;
        if (aaa % 30 == 0) {
@@ -1906,7 +2004,7 @@
                }
            }
        }
        */
    }
    void CMaster::connectEquipments()
@@ -2291,11 +2389,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);
@@ -2409,8 +2515,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();
@@ -2553,13 +2682,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;
        // æ–‡ä»¶å¤´
@@ -2612,10 +2737,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()
@@ -2654,6 +2802,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;
@@ -2758,6 +2907,77 @@
        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;
@@ -2849,4 +3069,116 @@
        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) ä¸ºé€šé“设置“曲线名称”
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 1, "气囊压力");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 2, "上腔压力");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 3, "管道真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 4, "腔体真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 5, "上腔温度1");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 6, "上腔温度2");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 7, "上腔温度3");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 8, "上腔温度4");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 9, "上腔温度5");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 10, "上腔温度6");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 11, "下腔温度1");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 12, "下腔温度2");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 13, "下腔温度3");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 14, "下腔温度4");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 15, "下腔温度5");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, 16, "下腔温度6");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 1, "气囊压力");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 2, "上腔压力");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 3, "管道真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 4, "腔体真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 5, "上腔温度1");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 6, "上腔温度2");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 7, "上腔温度3");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 8, "上腔温度4");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 9, "上腔温度5");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 10, "上腔温度6");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 11, "下腔温度1");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 12, "下腔温度2");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 13, "下腔温度3");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 14, "下腔温度4");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 15, "下腔温度5");
            m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, 16, "下腔温度6");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 1, "A腔真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 2, "A腔温控1");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 3, "A腔温控2");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 4, "A腔温控4");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 5, "A腔温控5");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 6, "A腔温控6");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 7, "A腔温控7");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 8, "B腔真空规值");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 9, "B腔温控1");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 10, "B腔温控2");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 11, "B腔温控4");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 12, "B腔温控5");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 13, "B腔温控6");
            m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, 14, "B腔温控7");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 1, "A烘烤温控1");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 2, "A烘烤温控2");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 3, "A烘烤温控4");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 4, "A烘烤温控5");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 5, "A烘烤温控6");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 6, "A烘烤温控7");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 7, "B烘烤温控1");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 8, "B烘烤温控2");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 9, "B烘烤温控4");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 10, "B烘烤温控5");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 11, "B烘烤温控6");
            m_pCollector->buffersSetChannelName(EQ_ID_BAKE_COOLING, 12, "B烘烤温控7");
        }
    }
}
SourceCode/Bond/Servo/CMaster.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); }
@@ -190,6 +194,8 @@
        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();
@@ -256,6 +262,10 @@
        int m_nTestFlag;
        std::list<CGlass*> m_bufGlass;
    private:
        Collector* m_pCollector = nullptr;
        void CreateDAQBridgeServer();
    };
}
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -661,106 +661,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;
    };
    // â˜…★★ è¿™é‡Œæ˜¯å…³é”®ä¿®å¤ï¼šæŽ¥æ”¶â€œconst 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();
    }
    // ä¹‹åŽæ­£å¸¸æŒ‰ 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(buddy å€™é€‰æ± ï¼Œå…è®¸å¤šä¸ªï¼‰
    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) ä¸¥æ ¼åŒ¹é…ï¼šCassette/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 +834,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 +1011,8 @@
        ++zebra;
    }
    // ä¸€æ¬¡æ€§é‡ç»˜
    m_listCtrl.RebuildVisible();
#endif
    // ä¸Šä¸€é¡µ / ä¸‹ä¸€é¡µ
    UpdatePageControls();
@@ -1162,7 +1357,7 @@
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) é€ä¸ªå¤„理 WIP:已存在 -> å°±åœ°æ›´æ–°ï¼›å¿…要时“只对根补子项”
    //                 ä¸å­˜åœ¨ -> ä¼˜å…ˆæŒ‚到 buddy å®¹å™¨ï¼›å¦åˆ™è§¦å‘整页重建(新根保持顶部)
    //                 ä¸å­˜åœ¨ -> æŒ‚到 buddy å®¹å™¨ï¼›è‹¥ buddy ä¸åœ¨å¯è§è¡¨ï¼Œè§¦å‘全量重建(保证 WIP é¡¶éƒ¨ï¼‰
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
@@ -1205,7 +1400,7 @@
                }
            }
            // â€”— åªå¯¹â€œæ ¹èŠ‚ç‚¹â€è¡¥å­é¡¹ï¼Œä¸”ä»…å½“ buddy å°šæœªå‡ºçŽ°åœ¨å¯è§è¡¨ï¼Œä¸”æ ¹ä¸‹ä¹Ÿæ²¡æœ‰è¯¥ buddy â€”—
            // â€”— åªå¯¹â€œæ ¹èŠ‚ç‚¹â€è¡¥å­é¡¹ â€”—
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                auto itRoot = wipRootById.find(cid);
@@ -1227,7 +1422,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 +1465,9 @@
                        }
                    }
                }
                // è‹¥å½“前是“子节点”,不在这里调整父子关系;让“关系变化”走全量重建
            }
            else {
                // æ²¡æœ‰ buddy:如果容器下现在有子,也算关系变化,触发重建
                // æ²¡ buddy ä½†å®¹å™¨ä¸‹æœ‰å­ -> å…³ç³»å˜åŒ–,触发全量重建
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
@@ -1284,8 +1478,6 @@
        }
        else {
            // (B) ä¸å­˜åœ¨ï¼šæ–°å¢ž
            //   å…ˆå°è¯•“挂到 buddy çš„容器根”下面;
            //   è‹¥ buddy ä¸åœ¨å½“前可见表,则触发全量重建(保证 WIP é¡¶éƒ¨ï¼‰ã€‚
            SERVO::CGlass* b = g->getBuddy();
            CExpandableListCtrl::Node* container = nullptr;
@@ -1303,7 +1495,6 @@
            }
            if (container) {
                // buddy å®¹å™¨å­˜åœ¨ï¼šæŠŠ g ä½œä¸ºâ€œå­è¡Œâ€æŒ‚上去(避免重复)
                CString cidCs = g->getID().c_str();
                if (!NodeHasChildWithClassId(container, cidCs)) {
                    if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
SourceCode/Bond/Servo/CPagePortProperty.cpp
@@ -196,8 +196,12 @@
    CMsgDlg msgDlg("请等待", "正在操作,请等待...");
    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()
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)
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;
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();
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);
    // åŠ è½½è­¦å‘Šä¿¡æ¯
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();
SourceCode/Bond/Servo/ProcessJob.cpp
@@ -186,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;
@@ -273,6 +275,31 @@
            cs.slots = std::move(slots);
            m_carriers.emplace_back(std::move(cs));
        }
    }
    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 ---------
@@ -439,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;
    }
}
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 {
@@ -131,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,6 +163,10 @@
        // è¿½åŠ ä¸€ä¸ªè½½å…·
        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; }
@@ -211,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();
    };
}
SourceCode/Bond/Servo/Servo.rc
Binary files differ
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" />
@@ -381,6 +394,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>
SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -206,6 +206,27 @@
    <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" />
@@ -437,6 +458,45 @@
    <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" />
@@ -476,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>
SourceCode/Bond/Servo/ServoDlg.cpp
@@ -1039,21 +1039,17 @@
        }
    }
    else if (id == IDC_BUTTON_JOBS) {
        static int i = 0; i++;
        if (i % 2 == 0) {
            CControlJobManagerDlg dlg;
            dlg.DoModal();
        }
        else {
            CControlJobDlg dlg;
            dlg.SetControlJob(theApp.m_model.m_master.getControlJob());
            dlg.DoModal();
        }
        CControlJobDlg dlg;
        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) {
        theApp.m_model.getMaster().clearError();
        SERVO::CEFEM* pEFEM = (SERVO::CEFEM*)theApp.m_model.getMaster().getEquipment(EQ_ID_EFEM);
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;
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;
SourceCode/Bond/Servo/resource.h
Binary files differ
SourceCode/Bond/x64/Debug/EqsGraph.ini
@@ -1,6 +1,6 @@
[LoadPort 1]
Left=23
Top=88
Top=87
[LoadPort 2]
Left=23
Top=437
SourceCode/Bond/x64/Debug/Res/cassette_gray_32.ico
SourceCode/Bond/x64/Debug/Res/cassette_high_32.ico