mrDarker
2025-10-22 e8a27bb203fe2aff70390a5eca002d7438da9b0f
Merge branch 'clh' into liuyang
已添加42个文件
已修改52个文件
10079 ■■■■■ 文件已修改
.gitignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Document/EO2860AVA-101工艺参数(3).xlsx 补丁 | 查看 | 原始文档 | blame | 历史
Document/EO2860AVA-101工艺参数(4).xlsx 补丁 | 查看 | 原始文档 | 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/CBakeCooling.cpp 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CBonder.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CBonder.h 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotGrid.cpp 1083 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotGrid.h 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotSelector.cpp 673 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCarrierSlotSelector.h 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage1.cpp 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage1.h 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.cpp 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage2.h 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.cpp 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPage3.h 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPageBase.cpp 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CCjPageBase.h 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJob.cpp 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJob.h 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobDlg.cpp 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobDlg.h 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.cpp 652 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CControlJobManagerDlg.h 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CEquipment.cpp 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CEquipment.h 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CExpandableListCtrl.cpp 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CGlass.cpp 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CGlass.h 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CJobDataS.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CLoadPort.cpp 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMaster.cpp 611 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMaster.h 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMeasurement.cpp 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageCollectionEvent.cpp 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageGlassList.cpp 791 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageGlassList.h 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPagePortProperty.cpp 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageReport.cpp 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageVarialbles.cpp 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CParam.cpp 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CServoUtilsTool.cpp 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CServoUtilsTool.h 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CVacuumBake.cpp 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ClientListDlg.cpp 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ClientListDlg.h 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Configuration.cpp 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Configuration.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/GlassJson.cpp 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/GlassLogDb.cpp 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/GlassLogDb.h 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/GroupLabel.cpp 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/GroupLabel.h 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Model.cpp 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Model.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/PageLog.cpp 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProcessJob.cpp 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProcessJob.h 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.cpp 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.rc 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj.filters 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj.user 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ServoDlg.cpp 160 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ServoDlg.h 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/TerminalDisplayDlg.cpp 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ToolUnits.cpp 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ToolUnits.h 1 ●●●● 补丁 | 查看 | 原始文档 | 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
Document/EO2860AVA-101¹¤ÒÕ²ÎÊý(3).xlsx
Binary files differ
Document/EO2860AVA-101¹¤ÒÕ²ÎÊý(4).xlsx
Binary files differ
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/CBakeCooling.cpp
@@ -487,7 +487,53 @@
    int CBakeCooling::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
    {
        return parsingParams(pszData, size, params);
        ASSERT(pszData);
        if (size < 250) return 0;
        int i = 0, v;
        // 1.工艺参数序号
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
        params.push_back(CParam("工艺参数序号", "", this->getName().c_str(), v));
        i += 2;
        if (v == 1) {
            // 2.A_腔烘烤时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("A_腔烘烤时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
            // 3.A_烘烤温度设定
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("A_烘烤温度设定", "", this->getName().c_str(), v * 0.1f));
            i += 2;
        }
        else if (v == 2) {
            // 2.A_腔冷却时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("A_腔冷却时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
        }
        else if (v == 3) {
            // 2.A_腔烘烤时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("B_腔烘烤时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
            // 3.A_烘烤温度设定
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("B_烘烤温度设定", "", this->getName().c_str(), v * 0.1f));
            i += 2;
        }
        else if (v == 4) {
            // 2.A_腔冷却时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("B_腔冷却时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
        }
        return (int)params.size();
    }
    int CBakeCooling::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
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/CCarrierSlotGrid.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1083 @@
#include "stdafx.h"
#include "CCarrierSlotGrid.h"
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
#define SAFE_PORT(p) ((p) >= 0 && (p) < (int)m_ports.size())
#define SAFE_SLOT(s) ((s) >= 0 && (s) < m_nSlots)
static bool       s_gdiplusInited = false;
static ULONG_PTR  s_gdiplusToken = 0;
static void EnsureGdiplus()
{
    if (!s_gdiplusInited)
    {
        GdiplusStartupInput in;
        if (GdiplusStartup(&s_gdiplusToken, &in, nullptr) == Ok)
            s_gdiplusInited = true;
    }
}
BEGIN_MESSAGE_MAP(CCarrierSlotGrid, CWnd)
    ON_WM_ERASEBKGND()
    ON_WM_PAINT()
    ON_WM_CREATE()
    ON_WM_SIZE()
    ON_WM_HSCROLL()
    ON_WM_VSCROLL()          // â˜… æ–°å¢ž
    ON_WM_LBUTTONDOWN()
    ON_WM_LBUTTONUP()
    ON_WM_MOUSEWHEEL()
    ON_WM_MOUSEMOVE()
    ON_WM_SETCURSOR()
    ON_WM_SHOWWINDOW()
    ON_WM_WINDOWPOSCHANGED()
END_MESSAGE_MAP()
CCarrierSlotGrid::CCarrierSlotGrid() {}
CCarrierSlotGrid::~CCarrierSlotGrid()
{
    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
}
void CCarrierSlotGrid::PreSubclassWindow()
{
    CWnd::PreSubclassWindow();
    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
    EnsureFonts();
    EnsureGdiplus();
    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
int CCarrierSlotGrid::OnCreate(LPCREATESTRUCT cs)
{
    if (CWnd::OnCreate(cs) == -1) return -1;
    if (GetParent() && GetParent()->GetFont()) SetFont(GetParent()->GetFont());
    EnsureFonts();
    EnsureGdiplus();
    return 0;
}
void CCarrierSlotGrid::EnsureFonts()
{
    if ((HFONT)m_fntText) return;
    LOGFONT lf{}; bool ok = false;
    if (GetParent() && GetParent()->GetFont()) { GetParent()->GetFont()->GetLogFont(&lf); ok = true; }
    else if (GetFont()) { GetFont()->GetLogFont(&lf); ok = true; }
    else {
        NONCLIENTMETRICS ncm = { 0 }; ncm.cbSize = sizeof(NONCLIENTMETRICS);
        if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0)) { lf = ncm.lfMessageFont; ok = true; }
    }
    if (!ok) { CFont* pDef = CFont::FromHandle((HFONT)GetStockObject(DEFAULT_GUI_FONT)); pDef->GetLogFont(&lf); }
    m_fntText.CreateFontIndirect(&lf);
    LOGFONT lfb = lf; lfb.lfWeight = FW_SEMIBOLD;  m_fntBold.CreateFontIndirect(&lfb);
    LOGFONT lfs = lf; lfs.lfHeight = (LONG)(lf.lfHeight * 0.9); if (lfs.lfHeight == 0) lfs.lfHeight = lf.lfHeight - 1; m_fntSmall.CreateFontIndirect(&lfs);
}
void CCarrierSlotGrid::InitGrid(int nPorts, int nSlots)
{
    ASSERT(nPorts >= 1 && nSlots >= 1);
    m_ports.assign(nPorts, PortColumn{});
    m_portColCXs.assign(nPorts, 180);
    m_nSlots = nSlots;
    for (auto& pc : m_ports) pc.slots.assign(m_nSlots, SlotCell{});
    UpdateScrollRange();
    Invalidate(FALSE);
}
void CCarrierSlotGrid::SetColumnWidths(int slotColWidth, int portColWidth)
{
    if (slotColWidth > 0) m_slotColCX = slotColWidth;
    if (portColWidth > 0)
        for (size_t i = 0; i < m_portColCXs.size(); ++i) m_portColCXs[i] = portColWidth;
    UpdateScrollRange();
    Invalidate(FALSE);
}
void CCarrierSlotGrid::SetRowHeight(int cy) { m_rowHeight = max(18, min(cy, 64)); UpdateScrollRange(); Invalidate(FALSE); }
void CCarrierSlotGrid::SetHeaderHeight(int cy) { m_headerCY = max(20, min(cy, 48)); UpdateScrollRange(); Invalidate(FALSE); }
void CCarrierSlotGrid::SetShowMaterialToggle(BOOL bShow) { m_bShowMatToggle = bShow; Invalidate(FALSE); }
void CCarrierSlotGrid::SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName)
{
    if (!SAFE_PORT(portIndex)) return;
    if (portName)    m_ports[portIndex].portName = portName;
    if (carrierName) m_ports[portIndex].carrierName = carrierName;
    Invalidate(FALSE);
}
void CCarrierSlotGrid::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
{
    if (!SAFE_PORT(portIndex)) return;
    auto& pc = m_ports[portIndex];
    pc.allocated = !!allocated;
    pc.allocatedBy = byName ? byName : _T("");
    if (pc.allocated)
        for (auto& cell : pc.slots) cell.checked = false;
    Invalidate(FALSE);
}
BOOL CCarrierSlotGrid::IsPortAllocated(int portIndex) const
{
    if (!SAFE_PORT(portIndex)) return FALSE; return m_ports[portIndex].allocated;
}
void CCarrierSlotGrid::SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    auto& cell = m_ports[portIndex].slots[slotIndex];
    cell.hasGlass = !!hasGlass;
    cell.coreId = coreId ? coreId : _T("");
    cell.material = (material == MAT_G2) ? MAT_G2 : MAT_G1;
    if (!cell.hasGlass) cell.checked = false;
    Invalidate(FALSE);
}
void CCarrierSlotGrid::SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify/* = FALSE*/)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    auto& pc = m_ports[portIndex];
    if (pc.allocated) return;
    auto& cell = pc.slots[slotIndex];
    if (!cell.hasGlass) return;
    cell.checked = !!checked;
    if(bNotify) NotifySelectionChanged(portIndex, slotIndex, cell.checked);
    Invalidate(FALSE);
}
BOOL CCarrierSlotGrid::GetSlotChecked(int portIndex, int slotIndex) const
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return FALSE;
    return m_ports[portIndex].slots[slotIndex].checked ? TRUE : FALSE;
}
int CCarrierSlotGrid::GetSlotMaterialType(int portIndex, int slotIndex) const
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
    return m_ports[portIndex].slots[slotIndex].material;
}
void CCarrierSlotGrid::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    auto& pc = m_ports[portIndex];
    if (pc.allocated) return;
    auto& cell = pc.slots[slotIndex];
    if (!cell.hasGlass) return;
    int mt = (material == MAT_G2) ? MAT_G2 : MAT_G1;
    if (cell.material != mt) {
        cell.material = mt;
        if (bNotify) NotifyMaterialChanged(portIndex, slotIndex, cell.material);
        Invalidate(FALSE);
    }
}
CString CCarrierSlotGrid::GetDisplayId(int portIndex, int slotIndex) const
{
    CString s(_T("—"));
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return s;
    const auto& cell = m_ports[portIndex].slots[slotIndex];
    if (!cell.hasGlass) return s;
    s.Format(_T("G%d-%s"), (cell.material == MAT_G2) ? 2 : 1, cell.coreId.GetString());
    return s;
}
void CCarrierSlotGrid::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
{
    if (!SAFE_PORT(portIndex)) return;
    auto& pc = m_ports[portIndex];
    if (pc.allocated) return;
    for (int r = 0; r < m_nSlots; ++r) {
        auto& cell = pc.slots[r];
        if (!cell.hasGlass) continue;
        if (cell.checked != !!checked) {
            cell.checked = !!checked;
            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
        }
    }
    Invalidate(FALSE);
}
void CCarrierSlotGrid::RebuildTexts() { Invalidate(FALSE); }
CSize CCarrierSlotGrid::CalcBestClientSize(int nSlotsOverride) const
{
    const int slots = (nSlotsOverride > 0) ? nSlotsOverride : m_nSlots;
    int w = m_slotColCX;
    for (int cx : m_portColCXs) w += cx;
    int h = m_headerCY + slots * m_rowHeight;
    return CSize(w, h);
}
CSize CCarrierSlotGrid::CalcBestWindowSize(BOOL includeNonClient,
    int nSlotsOverride,
    int extraPadX,
    int extraPadY) const
{
    // 1) åŸºç¡€å®¢æˆ·åŒºå°ºå¯¸ï¼ˆå«æˆ‘们在客户区画的 1px è¾¹æ¡†ï¼šå·¦å³+2/上下+2)
    const CSize content = CalcBestClientSize(nSlotsOverride);
    // 2) å– DPI、滚动条尺寸(尽量用 ForDpi,回退到普通)
    UINT dpi = 96;
#if (_WIN32_WINNT >= 0x0603)
    if (m_hWnd) {
        HMODULE hUser32 = ::GetModuleHandleW(L"user32.dll");
        if (hUser32) {
            typedef UINT(WINAPI* PFN_GETDPIFORWINDOW)(HWND);
            auto pGetDpiForWindow = (PFN_GETDPIFORWINDOW)::GetProcAddress(hUser32, "GetDpiForWindow");
            if (pGetDpiForWindow) dpi = pGetDpiForWindow(m_hWnd);
        }
    }
#endif
    int cxVScroll = ::GetSystemMetrics(SM_CXVSCROLL);
    int cyHScroll = ::GetSystemMetrics(SM_CYHSCROLL);
#if (_WIN32_WINNT >= 0x0A00) // Win10: å¯ç”¨ GetSystemMetricsForDpi
    HMODULE hUser32_2 = ::GetModuleHandleW(L"user32.dll");
    if (hUser32_2) {
        typedef int (WINAPI* PFN_GSMFD)(int, UINT);
        auto pGsmForDpi = (PFN_GSMFD)::GetProcAddress(hUser32_2, "GetSystemMetricsForDpi");
        if (pGsmForDpi) {
            cxVScroll = pGsmForDpi(SM_CXVSCROLL, dpi);
            cyHScroll = pGsmForDpi(SM_CYHSCROLL, dpi);
        }
    }
#endif
    // 3) DPI è‡ªé€‚应安全余量(避免取整误差/主题差异)
    const int autoPad = max(1, MulDiv(2, (int)dpi, 96)); // çº¦ç­‰äºŽ 2px@96DPI
    const int padX = (extraPadX >= 0) ? extraPadX : autoPad;
    const int padY = (extraPadY >= 0) ? extraPadY : autoPad;
    // 4) è¿­ä»£ï¼šè€ƒè™‘滚动条相互影响,直到稳定不需要滚动条
    int needCx = content.cx + padX;
    int needCy = content.cy + padY;
    while (true) {
        bool needV = (GetTotalContentWidth() > needCx);                      // å®½ä¸å¤Ÿâ†’会出现横向滚动条?(注意:横条占高度)
        bool needH = (m_headerCY + (nSlotsOverride > 0 ? nSlotsOverride : m_nSlots) * m_rowHeight + 2 /*客户区边框*/ > needCy); // é«˜ä¸å¤Ÿâ†’会出现纵条?(纵条占宽度)
        // æ³¨æ„ï¼šå‡ºçŽ°â€œçºµå‘æ¡â€ä¼šå‡å°‘å¯ç”¨å®½åº¦ï¼›å‡ºçŽ°â€œæ¨ªå‘æ¡â€ä¼šå‡å°‘å¯ç”¨é«˜åº¦
        // æˆ‘们目标是让“即使扣掉这些占位”后也仍然 >= å†…容尺寸
        int adjCx = content.cx + padX + (needH ? cxVScroll : 0);
        int adjCy = content.cy + padY + (needV ? cyHScroll : 0);
        if (adjCx <= needCx && adjCy <= needCy) break; // ç¨³å®šï¼šå½“前 needCx/needCy è¶³å¤Ÿ
        needCx = max(needCx, adjCx);
        needCy = max(needCy, adjCy);
    }
    if (!includeNonClient) return CSize(needCx, needCy);
    // 5) æŠŠâ€œç†æƒ³å®¢æˆ·åŒºå°ºå¯¸â€æ¢ç®—成窗口外框尺寸(去掉 WS_H/VSCROLL åšæ¢ç®—)
    RECT rc = { 0, 0, needCx, needCy };
    DWORD style = GetStyle();          // âœ… ç”¨çœŸå®žæ ·å¼ï¼Œè®©ç³»ç»ŸæŠŠæ»šåŠ¨æ¡éžå®¢æˆ·åŒºä¸€å¹¶ç®—è¿›åŽ»
    DWORD exStyle = GetExStyle();
    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
    return CSize(rc.right - rc.left, rc.bottom - rc.top);
}
void CCarrierSlotGrid::DisableSystemScrollbars()
{
    // åŽ»æŽ‰æ ·å¼
    ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
    // éšè—ï¼ˆå…¼å®¹æ€§ï¼‰
    ShowScrollBar(SB_HORZ, FALSE);
    ShowScrollBar(SB_VERT, FALSE);
    // æ¸…空滚动信息
    SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL; si.nMin = 0; si.nMax = 0; si.nPage = 0; si.nPos = 0;
    SetScrollInfo(SB_HORZ, &si, TRUE);
    SetScrollInfo(SB_VERT, &si, TRUE);
    // è®©éžå®¢æˆ·åŒºé‡ç®—
    SetWindowPos(nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
    Invalidate(FALSE);
}
// æ³¨æ„ï¼šè¿™é‡Œç”¨â€œæ— æ»šåŠ¨æ¡æ ·å¼â€æ¥æ¢ç®—çª—å£å¤–æ¡†å°ºå¯¸ï¼Œç¡®ä¿å®¢æˆ·åŒº=内容尺寸 + æˆ‘们客户区边框
void CCarrierSlotGrid::ResizeWindowToFitAll(BOOL includeNonClient, int nSlotsOverride)
{
    // è®¡ç®—内容所需客户区(CalcBestClientSize å†…已包含我们客户区1px边框的 +2)
    CSize need = CalcBestClientSize(nSlotsOverride);
    if (!includeNonClient) {
        SetWindowPos(nullptr, 0, 0, need.cx, need.cy,
            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
        return;
    }
    RECT rc = { 0, 0, need.cx, need.cy };
    DWORD style = GetStyle() & ~(WS_HSCROLL | WS_VSCROLL); // â† ç”¨â€œæ— æ»šåŠ¨æ¡â€çš„æ ·å¼æ¥æ¢ç®—
    DWORD exStyle = GetExStyle();
    ::AdjustWindowRectEx(&rc, style, FALSE, exStyle);
    int w = rc.right - rc.left;
    int h = rc.bottom - rc.top;
    SetWindowPos(nullptr, 0, 0, w, h,
        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
void CCarrierSlotGrid::SetNoScrollbarsMode(BOOL enable)
{
    m_noScrollbars = !!enable;
    if (m_noScrollbars) {
        // 1) åç§»æ¸…é›¶
        m_scrollX = 0;
        m_scrollY = 0;
        // 2) åŽ»æŽ‰æ ·å¼å¹¶éšè—æ¡
        ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
        ShowScrollBar(SB_BOTH, FALSE);
        // 3) æ¸…空滚动信息(即便资源里原本带了样式,也不再影响)
        SCROLLINFO si{ sizeof(SCROLLINFO) }; si.fMask = SIF_ALL;
        SetScrollInfo(SB_HORZ, &si, TRUE);
        SetScrollInfo(SB_VERT, &si, TRUE);
        // 4) é€šçŸ¥ç³»ç»Ÿéžå®¢æˆ·åŒºåˆ·æ–°ï¼Œç¡®ä¿æ¡è¢«å½»åº•移除
        SetWindowPos(nullptr, 0, 0, 0, 0,
            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
        Invalidate(FALSE);
    }
    else {
        // é€€å‡ºæ— æ»šåŠ¨æ¡æ¨¡å¼ï¼šä»…æ¢å¤æ ·å¼ï¼Œå®žé™…èŒƒå›´ä¼šåœ¨ UpdateScrollRange ä¸­é‡æ–°è®¾ç½®
        ModifyStyle(0, WS_HSCROLL | WS_VSCROLL, 0);
        SetWindowPos(nullptr, 0, 0, 0, 0,
            SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
        UpdateScrollRange();
        Invalidate(FALSE);
    }
}
void CCarrierSlotGrid::FitWindowToContentNoScroll(BOOL includeNonClient, int nSlotsOverride)
{
    // ç¡®ä¿å·²å¤„于“无滚动条模式”,防止系统在 AdjustWindowRectEx æ—¶é¢„留滚动条非客户区
    SetNoScrollbarsMode(TRUE);
    // ä½ è‡ªå·±çš„ CalcBestClientSize å·²åŒ…含客户区 1px è¾¹æ¡†(+2)的修正
    CSize needCli = CalcBestClientSize(nSlotsOverride);
    if (!includeNonClient) {
        SetWindowPos(nullptr, 0, 0, needCli.cx, needCli.cy,
            SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
        return;
    }
    RECT rc{ 0, 0, needCli.cx, needCli.cy };
    // æ³¨æ„ï¼šæ­¤æ—¶çª—口样式已经没有 WS_H/VSCROLL äº†â€”—用真实样式换算即可
    ::AdjustWindowRectEx(&rc, GetStyle(), FALSE, GetExStyle());
    const int w = rc.right - rc.left;
    const int h = rc.bottom - rc.top;
    SetWindowPos(nullptr, 0, 0, w, h,
        SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
// ---------- å‡ ä½• ----------
CRect CCarrierSlotGrid::GetClientRectNoSB() const
{
    CRect rc; GetClientRect(&rc); return rc;
}
CRect CCarrierSlotGrid::GetHeaderRect() const
{
    CRect rc = GetClientRectNoSB(); rc.bottom = rc.top + m_headerCY; return rc;
}
int CCarrierSlotGrid::GetTotalContentWidth() const
{
    int w = m_slotColCX;
    for (int cx : m_portColCXs) w += cx;
    return w;
}
CRect CCarrierSlotGrid::GetHeaderItemRect(int iItem) const
{
    CRect rcHeader = GetHeaderRect();
    int x = rcHeader.left - m_scrollX;
    if (iItem == 0)
        return CRect(x, rcHeader.top, x + m_slotColCX, rcHeader.bottom);
    x += m_slotColCX;
    for (int c = 1; c < iItem; ++c) x += m_portColCXs[c - 1];
    int w = m_portColCXs[iItem - 1];
    return CRect(x, rcHeader.top, x + w, rcHeader.bottom);
}
BOOL CCarrierSlotGrid::GetCellRect(int row, int sub, CRect& rc) const
{
    CRect cli = GetClientRectNoSB();
    int y0 = cli.top + m_headerCY - m_scrollY;
    int top = y0 + row * m_rowHeight;
    int bottom = top + m_rowHeight;
    if (bottom <= cli.top + m_headerCY || top >= cli.bottom) return FALSE;
    int x = cli.left - m_scrollX;
    if (sub == 0) { rc = CRect(x, top, x + m_slotColCX, bottom); return TRUE; }
    x += m_slotColCX;
    for (int c = 1; c < sub; ++c) x += m_portColCXs[c - 1];
    int w = m_portColCXs[sub - 1];
    rc = CRect(x, top, x + w, bottom);
    return TRUE;
}
CRect CCarrierSlotGrid::GetHeaderCheckboxRect(int iItem) const
{
    CRect rItem = GetHeaderItemRect(iItem);
    const int box = 16;
    const int padR = 6;
    int vpad = max(0, (rItem.Height() - box) / 2);
    return CRect(rItem.right - padR - box, rItem.top + vpad, rItem.right - padR, rItem.bottom - vpad);
}
CRect CCarrierSlotGrid::GetCheckboxRect(const CRect& cell) const
{
    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
    int leftPad = 8;
    int top = cell.top + (cell.Height() - sz) / 2;
    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
}
CRect CCarrierSlotGrid::GetMaterialTagRect(const CRect& cell) const
{
    int h = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
    int w = 32;
    int gap = 6;
    int rightPadForDot = 16;
    int top = cell.top + (cell.Height() - h) / 2;
    int right = cell.right - rightPadForDot - gap;
    return CRect(right - w, top, right, top + h);
}
CRect CCarrierSlotGrid::GetStatusDotRect(const CRect& cell) const
{
    int d = max(8, min(int(m_rowHeight * 0.42), 12));
    int rightPad = 6;
    int top = cell.top + (cell.Height() - d) / 2;
    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
}
void CCarrierSlotGrid::UpdateScrollRange()
{
    if (m_noScrollbars) {
        // ç¡®ä¿åç§»ä¸€ç›´ä¸º 0,不设任何滚动信息
        m_scrollX = 0;
        m_scrollY = 0;
        return;
    }
    CRect rc; GetClientRect(&rc);
    // åž‚ç›´
    const int contentH = m_headerCY + m_nSlots * m_rowHeight;
    const int pageY = max(1, rc.Height());
    const int maxPosY = max(0, contentH - pageY);
    m_scrollY = max(0, min(m_scrollY, maxPosY));
    SCROLLINFO siY = { sizeof(SCROLLINFO) };
    siY.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
    siY.nMin = 0; siY.nMax = contentH - 1; siY.nPage = pageY; siY.nPos = m_scrollY;
    SetScrollInfo(SB_VERT, &siY, TRUE);
    // æ°´å¹³
    const int contentW = GetTotalContentWidth();
    const int pageX = max(1, rc.Width());
    const int maxPosX = max(0, contentW - pageX);
    m_scrollX = max(0, min(m_scrollX, maxPosX));
    SCROLLINFO siX = { sizeof(SCROLLINFO) };
    siX.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
    siX.nMin = 0; siX.nMax = contentW - 1; siX.nPage = pageX; siX.nPos = m_scrollX;
    SetScrollInfo(SB_HORZ, &siX, TRUE);
}
// ---------- è¡¨å¤´åˆ†éš”线命中 ----------
int CCarrierSlotGrid::HitHeaderEdge(CPoint pt) const
{
    if (!m_bAllowResize) return -1;  // â† æ–°å¢ž
    if (!GetHeaderRect().PtInRect(pt)) return -1;
    const int tol = 4;
    int x = GetHeaderRect().left - m_scrollX + m_slotColCX;
    if (abs(pt.x - x) <= tol) return 0;
    int cum = GetHeaderRect().left - m_scrollX + m_slotColCX;
    for (int i = 0; i <= GetPortCount() - 2; ++i) {
        cum += m_portColCXs[i];
        if (abs(pt.x - cum) <= tol) return i + 1;
    }
    return -1;
}
// ---------- ç»˜åˆ¶ ----------
BOOL CCarrierSlotGrid::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; }
void CCarrierSlotGrid::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
{
    CBrush br(disabled ? RGB(245, 245, 245) : RGB(255, 255, 255));
    CPen   pen(PS_SOLID, 1, disabled ? RGB(200, 200, 200) : RGB(130, 130, 135));
    CBrush* pOldB = pDC->SelectObject(&br);
    CPen* pOldP = pDC->SelectObject(&pen);
    pDC->RoundRect(r, CPoint(3, 3));
    pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
    if (!checked) return;
    COLORREF c = disabled ? RGB(160, 160, 160) : RGB(40, 150, 90);
    CPen penTick(PS_SOLID, max(2, r.Height() / 8), c);
    CPen* pOld = pDC->SelectObject(&penTick);
    POINT p1 = { r.left + r.Width() * 2 / 9, r.top + r.Height() * 5 / 9 };
    POINT p2 = { r.left + r.Width() * 4 / 9, r.top + r.Height() * 7 / 9 };
    POINT p3 = { r.left + r.Width() * 7 / 9, r.top + r.Height() * 3 / 9 };
    pDC->MoveTo(p1); pDC->LineTo(p2); pDC->LineTo(p3);
    pDC->SelectObject(pOld);
}
void CCarrierSlotGrid::PaintTo(CDC* pDC)
{
    EnsureGdiplus();
    CRect cli = GetClientRectNoSB();
    pDC->FillSolidRect(cli, m_colBg);
    // Header
    CRect rh = GetHeaderRect();
    pDC->FillSolidRect(rh, ::GetSysColor(COLOR_BTNFACE));
    CPen penSep(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
    CPen* pOldPen = pDC->SelectObject(&penSep);
    pDC->MoveTo(rh.left, rh.bottom - 1); pDC->LineTo(rh.right, rh.bottom - 1);
    pDC->SelectObject(pOldPen);
    for (int i = 0; i <= GetPortCount(); ++i)
    {
        CRect rItem = GetHeaderItemRect(i);
        // ä¿®æ”¹ä¸ºï¼š
        if (i < GetPortCount()) { // â˜… æœ€åŽä¸€åˆ—不画分隔线
            CPen pen(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW));
            pOldPen = pDC->SelectObject(&pen);
            pDC->MoveTo(rItem.right - 1, rItem.top);
            pDC->LineTo(rItem.right - 1, rItem.bottom);
            pDC->SelectObject(pOldPen);
        }
        CString text;
        if (i == 0) {
            text = _T("Slot");
            CRect rt = rItem; rt.DeflateRect(6, 0, 6, 0);
            pDC->SetBkMode(TRANSPARENT);
            pDC->SelectObject(&m_fntBold);
            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
            pDC->DrawText(text, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
        }
        else {
            const auto& pc = m_ports[i - 1];
            int selected = 0; bool any = false, all = true;
            for (const auto& cell : pc.slots) {
                if (cell.hasGlass) { any = true; if (cell.checked) ++selected; else all = false; }
            }
            if (!any) all = false;
            CString leftTitle = pc.carrierName.IsEmpty()
                ? pc.portName
                : (pc.portName + _T(" (") + pc.carrierName + _T(")"));
            // å‹¾é€‰æ¡†é å³
            CRect rcCb = GetHeaderCheckboxRect(i);
            DrawFlatCheckbox(pDC, rcCb, all, pc.allocated);
            // è®¡æ•°è´´è¿‘勾选框左侧
            CString cnt; cnt.Format(_T("%d/%d"), selected, m_nSlots);
            SIZE szCnt{ 0,0 };
            { CFont* o = pDC->SelectObject(&m_fntBold);
            GetTextExtentPoint32(pDC->GetSafeHdc(), cnt, cnt.GetLength(), &szCnt);
            pDC->SelectObject(o); }
            const int gap = 6;
            CRect rcCnt(rcCb.left - gap - szCnt.cx, rItem.top, rcCb.left - gap, rItem.bottom);
            // å·¦ä¾§æ ‡é¢˜
            CRect rt = rItem; rt.DeflateRect(6, 0, (rItem.right - rcCnt.left) + 6, 0);
            pDC->SetBkMode(TRANSPARENT);
            pDC->SelectObject(&m_fntBold);
            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
            pDC->DrawText(leftTitle, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
            // è®¡æ•°
            pDC->SelectObject(&m_fntBold);
            pDC->SetTextColor(::GetSysColor(COLOR_BTNTEXT));
            pDC->DrawText(cnt, rcCnt, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
        }
    }
    // Cells
    for (int r = 0; r < m_nSlots; ++r)
    {
        for (int s = 0; s <= GetPortCount(); ++s)
        {
            CRect rc;
            if (!GetCellRect(r, s, rc)) continue;
            COLORREF bk = m_colBg;
            if (s >= 1) {
                int port = s - 1;
                if (port % 2 == 0) bk = m_colAlt;
                if (SAFE_PORT(port) && m_ports[port].allocated) bk = m_colLock;
            }
            pDC->FillSolidRect(rc, bk);
            CPen penMajor(PS_SOLID, (s >= 1 && ((s - 1) % 2 == 0)) ? 2 : 1, m_gridMajor);
            CPen* pOld = pDC->SelectObject(&penMajor);
            pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.right, rc.top);
            pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.left, rc.bottom);
            pDC->SelectObject(pOld);
            CPen penMinor(PS_SOLID, 1, m_gridMinor);
            pOld = pDC->SelectObject(&penMinor);
            if (s == GetPortCount()) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
            if (r == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
            pDC->SelectObject(pOld);
            if (s == 0) {
                CString sl; sl.Format(_T("Slot %d"), r + 1);
                CRect rt = rc; rt.DeflateRect(8, 0, 8, 0);
                pDC->SelectObject(&m_fntBold);
                pDC->SetBkMode(TRANSPARENT);
                pDC->SetTextColor(RGB(60, 60, 64));
                pDC->DrawText(sl, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
            }
            else {
                int port = s - 1;
                const auto& pc = m_ports[port];
                const auto& cell = pc.slots[r];
                CRect rChk = GetCheckboxRect(rc);
                DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
                CString t = GetDisplayId(port, r);
                CRect rText = rc;
                int leftPad = rChk.right + 6;
                CRect rDot = GetStatusDotRect(rc);
                CRect rTag = GetMaterialTagRect(rc);
                int rightPad = rc.right - min(rTag.left - 6, rDot.left - 6);
                rText.DeflateRect(leftPad - rc.left, 0, rightPad, 0);
                pDC->SelectObject(&m_fntText);
                pDC->SetBkMode(TRANSPARENT);
                pDC->SetTextColor(cell.hasGlass ? m_text : m_textDim);
                pDC->DrawText(t, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
                if (m_bShowMatToggle)
                {
                    CRect rT = GetMaterialTagRect(rc);
                    COLORREF crBorder = (cell.material == MAT_G2) ? RGB(180, 150, 220) : RGB(120, 160, 220);
                    COLORREF crFill = (cell.material == MAT_G2) ? RGB(243, 235, 250) : RGB(233, 240, 252);
                    COLORREF crText = (cell.material == MAT_G2) ? RGB(90, 60, 150) : RGB(50, 90, 160);
                    if (pc.allocated || !cell.hasGlass) { crBorder = RGB(210, 210, 210); crFill = RGB(245, 245, 245); crText = RGB(160, 160, 160); }
                    CBrush br(crFill); CPen tagPen(PS_SOLID, 1, crBorder);
                    CPen* pOldP = pDC->SelectObject(&tagPen);
                    CBrush* pOldB = pDC->SelectObject(&br);
                    pDC->RoundRect(rT, CPoint(6, 6));
                    pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
                    CString tx; tx.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
                    pDC->SelectObject(&m_fntSmall);
                    pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
                    pDC->DrawText(tx, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                }
                // çŠ¶æ€ç‚¹ï¼ˆGDI+ æŠ—锯齿)
                {
                    Graphics g(pDC->GetSafeHdc());
                    g.SetSmoothingMode(SmoothingModeAntiAlias);
                    COLORREF c = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
                    SolidBrush brush(Color(255, GetRValue(c), GetGValue(c), GetBValue(c)));
                    Pen outline(Color(255, 120, 120, 120), 1.f);
                    g.FillEllipse(&brush, rDot.left, rDot.top, rDot.Width(), rDot.Height());
                    g.DrawEllipse(&outline, (REAL)rDot.left, (REAL)rDot.top, (REAL)rDot.Width(), (REAL)rDot.Height());
                }
            }
        }
    }
// ===== åœ¨æ¯ä¸ªå·²åˆ†é…(allocated)的列中央绘制半透明 LOCK æ°´å°ï¼ˆç”¨ HDC+LOGFONT æž„造字体)=====
    {
        Gdiplus::Graphics g(pDC->GetSafeHdc());
        g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
        CRect cli = GetClientRectNoSB();
        CRect rh = GetHeaderRect();
        // å–当前 UI å­—体(优先粗体)
        LOGFONT lf{};
        if ((HFONT)m_fntBold)      m_fntBold.GetLogFont(&lf);
        else if ((HFONT)m_fntText) m_fntText.GetLogFont(&lf);
        for (int i = 0; i < GetPortCount(); ++i)
        {
            if (!m_ports[i].allocated) continue;
            // åˆ—矩形(除去表头,考虑水平滚动)
            CRect rCol = GetHeaderItemRect(i + 1);
            rCol.top = rh.bottom;
            rCol.bottom = cli.bottom;
            if (rCol.right <= cli.left || rCol.left >= cli.right || rCol.Height() <= 0) continue;
            // è‡ªé€‚应一个合适的像素高度
            int availW = rCol.Width() - 12;
            int availH = rCol.Height() - 12;
            int emPx = max(16, min(min(availW / 3, availH / 5), 72));
            if (emPx < 16) emPx = 16;
            // å­—号减半(并给个更低的兜底,避免太小)
            emPx = max(12, emPx / 2);
            // ç”¨ LOGFONTW + HDC æž„造 GDI+ å­—体
            LOGFONTW lfw{};
#ifdef UNICODE
            lfw = *reinterpret_cast<LOGFONTW*>(&lf);
#else
            lfw.lfHeight = lf.lfHeight;
            lfw.lfWidth = lf.lfWidth;
            lfw.lfEscapement = lf.lfEscapement;
            lfw.lfOrientation = lf.lfOrientation;
            lfw.lfWeight = lf.lfWeight;
            lfw.lfItalic = lf.lfItalic;
            lfw.lfUnderline = lf.lfUnderline;
            lfw.lfStrikeOut = lf.lfStrikeOut;
            lfw.lfCharSet = lf.lfCharSet;
            lfw.lfOutPrecision = lf.lfOutPrecision;
            lfw.lfClipPrecision = lf.lfClipPrecision;
            lfw.lfQuality = lf.lfQuality;
            lfw.lfPitchAndFamily = lf.lfPitchAndFamily;
            MultiByteToWideChar(CP_ACP, 0, lf.lfFaceName, -1, lfw.lfFaceName, LF_FACESIZE);
#endif
            lfw.lfHeight = -emPx;           // è´Ÿå€¼=按像素高度
            lfw.lfWeight = FW_BOLD;
            Gdiplus::Font gdifont(pDC->GetSafeHdc(), &lfw);    // â˜… åŠ ä¸Š Gdiplus::
            Gdiplus::StringFormat fmt;
            fmt.SetAlignment(Gdiplus::StringAlignmentCenter);
            fmt.SetLineAlignment(Gdiplus::StringAlignmentCenter);
            Gdiplus::Color col(140, 120, 100, 60);             // åŠé€æ˜Ž
            Gdiplus::SolidBrush brush(col);
            Gdiplus::RectF box((Gdiplus::REAL)rCol.left, (Gdiplus::REAL)rCol.top,
                (Gdiplus::REAL)rCol.Width(), (Gdiplus::REAL)rCol.Height());
            if (gdifont.GetLastStatus() == Gdiplus::Ok) {
                g.DrawString(L"LOCK", -1, &gdifont, box, &fmt, &brush);
            }
            else {
                Gdiplus::Font fallback(L"Arial", (Gdiplus::REAL)emPx, Gdiplus::FontStyleBold, Gdiplus::UnitPixel);
                g.DrawString(L"LOCK", -1, &fallback, box, &fmt, &brush);
            }
        }
    }
    // === å®¢æˆ·åŒºå†… 1px ç°è‰²è¾¹æ¡†ï¼ˆä¸åŒ…滚动条,但不会缺角/抢绘制)===
    {
        CRect cli; GetClientRect(&cli);
        // ç”¨ FrameRect æ›´ç¨³ï¼ˆé¿å…å³ä¸‹è§’丢线)
        CBrush br; br.CreateSolidBrush(::GetSysColor(COLOR_3DSHADOW));
        CRect r = cli;
        // æ³¨æ„ï¼šå®¢æˆ·åŒºåæ ‡æ˜¯ [0..width, 0..height];FrameRect ä¼šåœ¨å†…ä¾§ç”» 1px
        pDC->FrameRect(&r, &br);
        br.DeleteObject();
    }
}
void CCarrierSlotGrid::OnPaint()
{
    CPaintDC dc(this);
    CRect rc; GetClientRect(&rc);
    CDC mem; mem.CreateCompatibleDC(&dc);
    CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
    HGDIOBJ ob = mem.SelectObject(bmp);
    PaintTo(&mem);
    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &mem, 0, 0, SRCCOPY);
    mem.SelectObject(ob);
}
void CCarrierSlotGrid::OnSize(UINT nType, int cx, int cy)
{
    CWnd::OnSize(nType, cx, cy);
    UpdateScrollRange();
    Invalidate(FALSE);
    RedrawWindow(nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW);
}
void CCarrierSlotGrid::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
{
    if (m_noScrollbars) return; // â† æ–°å¢ž
    UNREFERENCED_PARAMETER(pBar);
    SCROLLINFO si = { sizeof(SCROLLINFO) };
    si.fMask = SIF_ALL;
    GetScrollInfo(SB_HORZ, &si);
    int pos = m_scrollX;
    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
    switch (nSBCode)
    {
    case SB_LINELEFT:  pos -= 30; break;
    case SB_LINERIGHT: pos += 30; break;
    case SB_PAGELEFT:  pos -= (int)si.nPage; break;
    case SB_PAGERIGHT: pos += (int)si.nPage; break;
    case SB_THUMBTRACK:
    case SB_THUMBPOSITION:
        pos = (int)si.nTrackPos;   // â˜… 32 ä½æ‹–动位置
        break;
    default:
        return;
    }
    pos = max(0, min(pos, maxPos));
    if (pos != m_scrollX) {
        m_scrollX = pos;
        SetScrollPos(SB_HORZ, m_scrollX);
        Invalidate(FALSE);
    }
}
void CCarrierSlotGrid::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
{
    if (m_noScrollbars) return; // â† æ–°å¢ž
    UNREFERENCED_PARAMETER(pBar);
    SCROLLINFO si = { sizeof(SCROLLINFO) };
    si.fMask = SIF_ALL;
    GetScrollInfo(SB_VERT, &si);
    int pos = m_scrollY;
    const int maxPos = max(0, (int)si.nMax - (int)si.nPage + 1);
    switch (nSBCode)
    {
    case SB_LINEUP:    pos -= m_rowHeight; break;
    case SB_LINEDOWN:  pos += m_rowHeight; break;
    case SB_PAGEUP:    pos -= (int)si.nPage; break;
    case SB_PAGEDOWN:  pos += (int)si.nPage; break;
    case SB_THUMBTRACK:
    case SB_THUMBPOSITION:
        pos = (int)si.nTrackPos;   // â˜… 32 ä½æ‹–动位置
        break;
    default:
        return;
    }
    pos = max(0, min(pos, maxPos));
    if (pos != m_scrollY) {
        m_scrollY = pos;
        SetScrollPos(SB_VERT, m_scrollY);
        Invalidate(FALSE);
    }
}
BOOL CCarrierSlotGrid::OnMouseWheel(UINT, short zDelta, CPoint)
{
    if (m_noScrollbars) return FALSE; // â† æ–°å¢žï¼šå½»åº•不滚
    int delta = (zDelta > 0 ? -1 : +1) * (m_rowHeight * 3);
    m_scrollY = max(0, m_scrollY + delta);
    UpdateScrollRange();
    Invalidate(FALSE);
    return TRUE;
}
void CCarrierSlotGrid::OnShowWindow(BOOL bShow, UINT nStatus)
{
    CWnd::OnShowWindow(bShow, nStatus);
    if (bShow) { UpdateScrollRange(); Invalidate(FALSE); }
}
void CCarrierSlotGrid::OnWindowPosChanged(WINDOWPOS* wp)
{
    CWnd::OnWindowPosChanged(wp);
    if (wp && (wp->flags & SWP_SHOWWINDOW)) { UpdateScrollRange(); Invalidate(FALSE); }
}
void CCarrierSlotGrid::OnLButtonDown(UINT nFlags, CPoint pt)
{
    // æ˜¯å¦æ‹–动列宽
    int edge = m_bAllowResize ? HitHeaderEdge(pt) : -1;  // â† ä¿®æ”¹
    if (edge >= 0)
    {
        m_bResizing = true;
        m_resizeEdge = edge;
        m_resizeStartX = pt.x;
        m_slotColCXStart = m_slotColCX;
        m_portColCXsStart = m_portColCXs;
        SetCapture();
        return;
    }
    // Header ç‚¹å‡»ï¼ˆä»…复选框区域)
    if (GetHeaderRect().PtInRect(pt))
    {
        for (int i = 1; i <= GetPortCount(); ++i)
        {
            CRect rItem = GetHeaderItemRect(i);
            if (!rItem.PtInRect(pt)) continue;
            CRect rcCb = GetHeaderCheckboxRect(i);
            if (!rcCb.PtInRect(pt)) return;
            int port = i - 1;
            if (!SAFE_PORT(port) || m_ports[port].allocated) return;
            bool any = false, all = true;
            for (auto& cell : m_ports[port].slots) {
                if (!cell.hasGlass) continue;
                any = true; if (!cell.checked) { all = false; break; }
            }
            if (!any) return;
            CheckAllInPort(port, all ? FALSE : TRUE, TRUE);
            return;
        }
        return;
    }
    // Cell ç‚¹å‡»
    CRect cli = GetClientRectNoSB();
    if (pt.y < cli.top + m_headerCY) return;
    int yIn = pt.y - (cli.top + m_headerCY) + m_scrollY;
    int row = yIn / m_rowHeight;
    if (!SAFE_SLOT(row)) return;
    int x = pt.x + m_scrollX - cli.left;
    int sub = 0;
    if (x < m_slotColCX) sub = 0;
    else {
        x -= m_slotColCX;
        sub = 1;
        for (size_t i = 0; i < m_portColCXs.size(); ++i) {
            if (x < m_portColCXs[i]) { sub = (int)i + 1; break; }
            x -= m_portColCXs[i];
            sub = (int)i + 2;
        }
        if (sub < 1 || sub > GetPortCount()) return;
    }
    if (sub == 0) return;
    int port = sub - 1;
    if (!SAFE_PORT(port)) return;
    auto& pc = m_ports[port];
    auto& cell = pc.slots[row];
    CRect rc; if (!GetCellRect(row, sub, rc)) return;
    if (pc.allocated || !cell.hasGlass) return;
    if (GetCheckboxRect(rc).PtInRect(pt))
    {
        cell.checked = !cell.checked;
        NotifySelectionChanged(port, row, cell.checked);
        Invalidate(FALSE);
        return;
    }
    if (m_bShowMatToggle && GetMaterialTagRect(rc).PtInRect(pt))
    {
        cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
        NotifyMaterialChanged(port, row, cell.material);
        Invalidate(FALSE);
        return;
    }
    CWnd::OnLButtonDown(nFlags, pt);
}
void CCarrierSlotGrid::OnLButtonUp(UINT nFlags, CPoint pt)
{
    if (m_bResizing)
    {
        ReleaseCapture();
        m_bResizing = false;
        m_resizeEdge = -1;
        UpdateScrollRange();
        Invalidate(FALSE);
    }
    CWnd::OnLButtonUp(nFlags, pt);
}
void CCarrierSlotGrid::OnMouseMove(UINT nFlags, CPoint pt)
{
    if (m_bResizing)
    {
        int dx = pt.x - m_resizeStartX;
        if (m_resizeEdge == 0)
        {
            int nw = max(m_slotColMin, m_slotColCXStart + dx);
            if (nw != m_slotColCX) { m_slotColCX = nw; UpdateScrollRange(); Invalidate(FALSE); }
        }
        else
        {
            int idx = m_resizeEdge - 1; // è°ƒæ•´ Port idx çš„宽度
            int nw = max(m_portColMin, m_portColCXsStart[idx] + dx);
            if (nw != m_portColCXs[idx]) { m_portColCXs[idx] = nw; UpdateScrollRange(); Invalidate(FALSE); }
        }
        return;
    }
    int edge = HitHeaderEdge(pt);
    if (edge != m_hitEdgeHover)
    {
        m_hitEdgeHover = edge;
        if (m_hitEdgeHover >= 0) ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
        else ::SetCursor(::LoadCursor(nullptr, IDC_ARROW));
    }
}
BOOL CCarrierSlotGrid::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    CPoint pt; ::GetCursorPos(&pt); ScreenToClient(&pt);
    if (m_bAllowResize && (m_bResizing || HitHeaderEdge(pt) >= 0))
    {
        ::SetCursor(::LoadCursor(nullptr, IDC_SIZEWE));
        return TRUE;
    }
    return CWnd::OnSetCursor(pWnd, nHitTest, message);
}
void CCarrierSlotGrid::NotifySelectionChanged(int port, int slot, BOOL checked)
{
    // å…¼å®¹æ—§çš„ WM_COMMAND(可留,也可注释掉)
    if (GetParent()) {
        const int code = 0x2001;
        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
    }
    // æ–°çš„ WM_NOTIFY,带上索引与状态
    if (GetParent()) {
        CSG_SEL_CHANGE nm{};
        nm.hdr.hwndFrom = m_hWnd;
        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
        nm.hdr.code = CSGN_SEL_CHANGED;
        nm.port = port;
        nm.slot = slot;
        nm.checked = checked;
        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
    }
}
void CCarrierSlotGrid::NotifyMaterialChanged(int port, int slot, int material)
{
    if (GetParent()) {
        const int code = 0x2002;
        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
    }
    if (GetParent()) {
        CSG_MAT_CHANGE nm{};
        nm.hdr.hwndFrom = m_hWnd;
        nm.hdr.idFrom = (UINT)GetDlgCtrlID();
        nm.hdr.code = CSGN_MAT_CHANGED;
        nm.port = port;
        nm.slot = slot;
        nm.material = material;
        GetParent()->SendMessage(WM_NOTIFY, nm.hdr.idFrom, (LPARAM)&nm);
    }
}
SourceCode/Bond/Servo/CCarrierSlotGrid.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,195 @@
#include "stdafx.h"
#pragma once
#include <vector>
#ifndef _AFX
#include <afxwin.h>
#endif
// æ”¾åˆ° CCarrierSlotGrid ç±»å®šä¹‰å‰æˆ–内部 public: åŒºéƒ½å¯
enum { CSGN_SEL_CHANGED = 1, CSGN_MAT_CHANGED = 2 };
struct CSG_SEL_CHANGE {
    NMHDR hdr;   // hdr.code = CSGN_SEL_CHANGED
    int   port;  // 0..GetPortCount()-1
    int   slot;  // 0..GetSlotCount()-1
    BOOL  checked;
};
struct CSG_MAT_CHANGE {
    NMHDR hdr;   // hdr.code = CSGN_MAT_CHANGED
    int   port;
    int   slot;
    int   material; // 1=G1, 2=G2
};
class CCarrierSlotGrid : public CWnd
{
public:
    enum MaterialType { MAT_G1 = 1, MAT_G2 = 2 };
    struct SlotCell {
        bool    hasGlass = false;
        CString coreId;
        int     material = MAT_G1;
        bool    checked = false;
    };
    struct PortColumn {
        CString               portName;
        CString               carrierName;
        bool                  allocated = false;
        CString               allocatedBy;
        std::vector<SlotCell> slots;
    };
public:
    CCarrierSlotGrid();
    virtual ~CCarrierSlotGrid();
    BOOL SubclassDlgItem(UINT nID, CWnd* pParent) {
        BOOL ok = CWnd::SubclassDlgItem(nID, pParent);
        if (ok) {
            if (pParent && pParent->GetFont()) SetFont(pParent->GetFont());
            EnsureFonts();
            Invalidate(FALSE);
        }
        return ok;
    }
    virtual void PreSubclassWindow() override;
    // åˆå§‹åŒ–
    void InitGrid(int nPorts, int nSlots);
    void SetColumnWidths(int slotColWidth, int portColWidth);
    void SetRowHeight(int cy);
    void SetHeaderHeight(int cy);
    void SetShowMaterialToggle(BOOL bShow);
    // è¯»/写
    int  GetPortCount() const { return (int)m_ports.size(); }
    int  GetSlotCount() const { return m_nSlots; }
    void SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName);
    void SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName = nullptr);
    BOOL IsPortAllocated(int portIndex) const;
    void SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material);
    void SetSlotChecked(int portIndex, int slotIndex, BOOL checked, BOOL bNotify = FALSE);
    BOOL GetSlotChecked(int portIndex, int slotIndex) const;
    int  GetSlotMaterialType(int portIndex, int slotIndex) const;
    void SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify = TRUE);
    CString GetDisplayId(int portIndex, int slotIndex) const;
    void    CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify = TRUE);
    void RebuildTexts();
    void EnableColumnResize(BOOL enable) { m_bAllowResize = !!enable; Invalidate(FALSE); }
    // è®¡ç®—最佳大小:
// - CalcBestClientSize:内容区域(不含滚动条/非客户区)刚好容纳表头+所有行、全部列
// - CalcBestWindowSize:在当前窗口样式下,将“内容大小”转换为窗口外框大小(会考虑 WS_BORDER/CLIENTEDGE ç­‰ï¼‰
//   é»˜è®¤æŒ‰â€œéšè—æ»šåŠ¨æ¡â€çš„ç›®æ ‡æ¥ç®—ï¼ˆå³ä¸æŠŠ WS_HSCROLL/WS_VSCROLL è®¡å…¥è°ƒæ•´ï¼‰
// è®¡ç®—最佳大小(支持可选安全边距,默认按 DPI çº¦ç­‰äºŽ 2px)
    CSize CalcBestClientSize(int nSlotsOverride = -1) const;
    CSize CalcBestWindowSize(BOOL includeNonClient = TRUE,
        int nSlotsOverride = -1,
        int extraPadX = -1,  // -1 è¡¨ç¤ºæŒ‰ DPI è‡ªåЍ
        int extraPadY = -1) const;
    // æ°¸ä¹…禁用系统滚动条(去掉样式并刷新非客户区)
    void DisableSystemScrollbars();
    // æŠŠçª—口尺寸调到正好容纳所有内容(不出现滚动条)
    void ResizeWindowToFitAll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
    // è¿›å…¥/退出无滚动条模式(去样式、清滚动、忽略滚动消息)
    void SetNoScrollbarsMode(BOOL enable);
    // åœ¨â€œæ— æ»šåŠ¨æ¡æ¨¡å¼â€ä¸‹ï¼ŒæŠŠçª—å£å°ºå¯¸è°ƒåˆ°åˆšå¥½å®¹çº³æ‰€æœ‰å†…å®¹ï¼ˆä¸å‡ºçŽ°æ»šåŠ¨æ¡ï¼‰
    void FitWindowToContentNoScroll(BOOL includeNonClient = TRUE, int nSlotsOverride = -1);
protected:
    // æ•°æ®
    int                     m_nSlots = 0;
    std::vector<PortColumn> m_ports;
    BOOL                    m_bShowMatToggle = TRUE;
    // å°ºå¯¸/滚动
    int m_rowHeight = 26;
    int m_headerCY = 28;
    int m_slotColCX = 100;
    std::vector<int> m_portColCXs;
    int m_scrollY = 0;
    int m_scrollX = 0;
    int m_slotColMin = 60;
    int m_portColMin = 80;
    // é¢œè‰²
    COLORREF m_colBg = RGB(255, 255, 255);
    COLORREF m_colAlt = RGB(240, 242, 245);
    COLORREF m_colLock = RGB(255, 244, 214);
    COLORREF m_gridMajor = RGB(210, 214, 220);
    COLORREF m_gridMinor = RGB(220, 224, 230);
    COLORREF m_text = RGB(40, 40, 40);
    COLORREF m_textDim = RGB(150, 150, 150);
    // å­—体
    CFont m_fntText;
    CFont m_fntBold;
    CFont m_fntSmall;
    // æ‹–动列宽
    bool m_bResizing = false;
    int  m_resizeEdge = -1;   // 0=Slot|Port1;1..N-1=Port i|i+1
    int  m_resizeStartX = 0;
    int  m_slotColCXStart = 0;
    std::vector<int> m_portColCXsStart;
    int  m_hitEdgeHover = -1;
    bool m_bAllowResize = true; // â† æ–°å¢žï¼šæ˜¯å¦å…è®¸æ‹–动列宽
    bool m_noScrollbars = false;   // â† æ–°å¢žï¼šæ— æ»šåŠ¨æ¡æ¨¡å¼
    // å·¥å…·
    void EnsureFonts();
    void UpdateScrollRange();
    int  GetTotalContentWidth() const;
    void NotifySelectionChanged(int port, int slot, BOOL checked);
    void NotifyMaterialChanged(int port, int slot, int material);
    // å‡ ä½•
    CRect GetClientRectNoSB() const;
    BOOL  GetCellRect(int row, int sub, CRect& rc) const; // sub: 0=Slot, 1..N=Port
    CRect GetHeaderRect() const;
    CRect GetHeaderItemRect(int iItem) const;
    CRect GetHeaderCheckboxRect(int iItem) const;
    CRect GetCheckboxRect(const CRect& cell) const;
    CRect GetMaterialTagRect(const CRect& cell) const;
    CRect GetStatusDotRect(const CRect& cell) const;
    BOOL IsColumnResizeEnabled() const { return m_bAllowResize ? TRUE : FALSE; }
    int   HitHeaderEdge(CPoint pt) const;
    // ç»˜åˆ¶
    void DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled);
    void PaintTo(CDC* pDC);
protected:
    // æ¶ˆæ¯
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg void OnPaint();
    afx_msg int  OnCreate(LPCREATESTRUCT lpCreateStruct);
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
    afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); // â˜… çºµå‘滚动条
    afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
    afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
    afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
    afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
    afx_msg void OnShowWindow(BOOL bShow, UINT nStatus);
    afx_msg void OnWindowPosChanged(WINDOWPOS* wp);
    DECLARE_MESSAGE_MAP()
};
SourceCode/Bond/Servo/CCarrierSlotSelector.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,673 @@
#include "stdafx.h"
#include "CCarrierSlotSelector.h"
#define SAFE_PORT(p)    ((p) >= 0 && (p) < (int)m_ports.size())
#define SAFE_SLOT(s)    ((s) >= 0 && (s) < m_nSlots)
#ifndef LVS_EX_DOUBLEBUFFER
#define LVS_EX_DOUBLEBUFFER 0x00010000
#endif
BEGIN_MESSAGE_MAP(CCarrierSlotSelector, CListCtrl)
    ON_WM_SHOWWINDOW()
    ON_WM_WINDOWPOSCHANGED()
    ON_WM_SIZE()
    ON_WM_ERASEBKGND()
    ON_WM_PAINT()                           // â˜… æ–°å¢ž
    ON_WM_LBUTTONDOWN()
    ON_WM_MOUSEMOVE()
    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CCarrierSlotSelector::OnCustomDraw)
END_MESSAGE_MAP()
CCarrierSlotSelector::CCarrierSlotSelector() {}
CCarrierSlotSelector::~CCarrierSlotSelector()
{
    if ((HFONT)m_fntText)  m_fntText.DeleteObject();
    if ((HFONT)m_fntBold)  m_fntBold.DeleteObject();
    if ((HFONT)m_fntSmall) m_fntSmall.DeleteObject();
    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
}
void CCarrierSlotSelector::PreSubclassWindow()
{
    CListCtrl::PreSubclassWindow();
    ModifyStyle(LVS_TYPEMASK, LVS_REPORT | LVS_SHOWSELALWAYS);
    ModifyStyle(LVS_OWNERDRAWFIXED | LVS_OWNERDATA, 0);
    DWORD ex = GetExtendedStyle();
    ex |= LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER;
    ex &= ~LVS_EX_GRIDLINES; // å…³é—­ç³»ç»Ÿç½‘格,改自绘
    SetExtendedStyle(ex);
    // è®©é»˜è®¤ç»˜åˆ¶ç”¨æˆ‘们的底色,进一步降低白底机会(即使哪处走了默认路径)
    ListView_SetBkColor(m_hWnd, m_colBgNorm);
    ListView_SetTextBkColor(m_hWnd, m_colBgNorm);
    EnsureFonts();
    SetRowHeight(m_rowHeight);
}
void CCarrierSlotSelector::OnShowWindow(BOOL bShow, UINT nStatus)
{
    CListCtrl::OnShowWindow(bShow, nStatus);
    if (bShow && !m_bFirstShown)
    {
        m_bFirstShown = TRUE;
        RedrawWindow(nullptr, nullptr,
            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
    }
}
void CCarrierSlotSelector::OnWindowPosChanged(WINDOWPOS* wp)
{
    CListCtrl::OnWindowPosChanged(wp);
    if (wp && (wp->flags & SWP_SHOWWINDOW))
    {
        RedrawWindow(nullptr, nullptr,
            RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW);
    }
}
void CCarrierSlotSelector::EnsureFonts()
{
    if (!(HFONT)m_fntText)
    {
        LOGFONT lf = { 0 };
        CFont* pSys = GetFont();
        if (pSys) pSys->GetLogFont(&lf);
        else { lf.lfHeight = -14; lstrcpy(lf.lfFaceName, _T("Segoe UI")); }
        m_fntText.CreateFontIndirect(&lf);
        lf.lfWeight = FW_SEMIBOLD;
        m_fntBold.CreateFontIndirect(&lf);
        lf.lfWeight = FW_NORMAL; lf.lfHeight = -12;
        m_fntSmall.CreateFontIndirect(&lf);
    }
}
void CCarrierSlotSelector::InitGrid(int nPorts, int nSlots)
{
    ASSERT(nPorts >= 1 && nSlots >= 1);
    m_ports.clear();
    m_ports.resize(nPorts);
    m_nSlots = nSlots;
    for (auto& pc : m_ports) pc.slots.resize(m_nSlots);
    SetRedraw(FALSE);
    DeleteAllItems();
    while (GetHeaderCtrl() && GetHeaderCtrl()->GetItemCount() > 0)
        DeleteColumn(0);
    InsertColumn(0, _T("Slot"), LVCFMT_LEFT, m_slotColWidth);
    for (int c = 0; c < nPorts; ++c)
    {
        CString col; col.Format(_T("Port %d"), c + 1);
        InsertColumn(c + 1, col, LVCFMT_LEFT, m_portColWidth);
        m_ports[c].portName = col;
        m_ports[c].carrierName.Empty();
    }
    UpdateRowCount();
    RebuildTexts();
    SetRedraw(TRUE);
    if (IsWindowVisible()) Invalidate(FALSE);
}
void CCarrierSlotSelector::SetColumnWidths(int slotColWidth, int portColWidth)
{
    if (slotColWidth > 0) m_slotColWidth = slotColWidth;
    if (portColWidth > 0) m_portColWidth = portColWidth;
    SetColumnWidth(0, m_slotColWidth);
    for (int c = 0; c < (int)m_ports.size(); ++c) SetColumnWidth(c + 1, m_portColWidth);
    if (IsWindowVisible()) Invalidate(FALSE);
}
void CCarrierSlotSelector::SetRowHeight(int cy)
{
    cy = max(18, min(cy, 64));
    m_rowHeight = cy;
    if ((HIMAGELIST)m_ilRowHeight) m_ilRowHeight.DeleteImageList();
    m_ilRowHeight.Create(1, m_rowHeight, ILC_COLOR32, 1, 1);
    // 1×cy é€æ˜Žä½å›¾
    CBitmap bmp; bmp.CreateBitmap(1, m_rowHeight, 1, 32, nullptr);
    m_ilRowHeight.Add(&bmp, RGB(0, 0, 0));
    SetImageList(&m_ilRowHeight, LVSIL_SMALL);
    if (IsWindowVisible()) Invalidate(FALSE);
}
void CCarrierSlotSelector::UpdateRowCount()
{
    int cur = GetItemCount();
    if (cur < m_nSlots)
    {
        for (int i = cur; i < m_nSlots; ++i)
        {
            CString s; s.Format(_T("Slot %d"), i + 1);
            InsertItem(i, s, 0); // è®¾ iImage=0,行高来自 small image list
        }
    }
    else if (cur > m_nSlots)
    {
        for (int i = cur - 1; i >= m_nSlots; --i) DeleteItem(i);
    }
    // å·²æœ‰è¡Œä¹Ÿç»Ÿä¸€è®¾ iImage=0
    LVITEM lvi = { 0 };
    lvi.mask = LVIF_IMAGE; lvi.iImage = 0;
    for (int i = 0; i < m_nSlots; ++i) { lvi.iItem = i; SetItem(&lvi); }
}
void CCarrierSlotSelector::RebuildTexts()
{
    // åˆ—头:显示 â€œå·²é€‰æ•°/总槽数 ï¼‹ [x]/[ ]”
    for (int c = 0; c < (int)m_ports.size(); ++c)
    {
        int selected = 0;
        int total = m_nSlots;
        bool anyCheckable = false;
        bool allChecked = true;
        for (int r = 0; r < m_nSlots; ++r)
        {
            const auto& cell = m_ports[c].slots[r];
            if (cell.hasGlass)
            {
                anyCheckable = true;
                if (cell.checked) ++selected; else allChecked = false;
            }
        }
        if (!anyCheckable) allChecked = false;
        CString head;
        CString chk = allChecked ? _T("[x]") : _T("[ ]");
        if (!m_ports[c].carrierName.IsEmpty())
            head.Format(_T("%s (%s)   %d/%d  %s%s"),
                m_ports[c].portName.GetString(), m_ports[c].carrierName.GetString(),
                selected, total,
                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
                chk.GetString());
        else
            head.Format(_T("%s   %d/%d  %s%s"),
                m_ports[c].portName.GetString(), selected, total,
                m_ports[c].allocated ? _T("[LOCK]  ") : _T(""),
                chk.GetString());
        LVCOLUMN lvc = { 0 }; lvc.mask = LVCF_TEXT; lvc.pszText = head.GetBuffer();
        SetColumn(c + 1, &lvc);
        head.ReleaseBuffer();
    }
    // è¡Œå¤´æ–‡æœ¬
    for (int r = 0; r < m_nSlots; ++r)
    {
        CString s; s.Format(_T("Slot %d"), r + 1);
        SetItemText(r, 0, s);
    }
    // å•元格临时文本
    for (int r = 0; r < m_nSlots; ++r)
        for (int c = 0; c < (int)m_ports.size(); ++c)
            SetItemText(r, c + 1, GetDisplayId(c, r));
}
void CCarrierSlotSelector::SetShowMaterialToggle(BOOL bShow)
{
    m_bShowMatToggle = bShow;
    if (IsWindowVisible()) Invalidate(FALSE);
}
void CCarrierSlotSelector::SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName)
{
    if (!SAFE_PORT(portIndex)) return;
    if (portName)    m_ports[portIndex].portName = portName;
    if (carrierName) m_ports[portIndex].carrierName = carrierName;
    RebuildTexts();
    if (IsWindowVisible()) Invalidate(FALSE);
}
void CCarrierSlotSelector::SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName)
{
    if (!SAFE_PORT(portIndex)) return;
    m_ports[portIndex].allocated = !!allocated;
    if (byName) m_ports[portIndex].allocatedBy = byName; else m_ports[portIndex].allocatedBy.Empty();
    if (allocated)
        for (int r = 0; r < m_nSlots; ++r) m_ports[portIndex].slots[r].checked = false;
    // åˆ·æ–°æ•´åˆ—
    for (int r = 0; r < m_nSlots; ++r)
    {
        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
        InvalidateRect(&rc, FALSE);
    }
    RebuildTexts();
}
BOOL CCarrierSlotSelector::IsPortAllocated(int portIndex) const
{
    if (!SAFE_PORT(portIndex)) return FALSE;
    return m_ports[portIndex].allocated;
}
void CCarrierSlotSelector::SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId, int material)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    auto& cell = m_ports[portIndex].slots[slotIndex];
    cell.hasGlass = !!hasGlass;
    cell.coreId = coreId ? coreId : _T("");
    cell.material = (material == MAT_G2) ? MAT_G2 : MAT_G1;
    if (!cell.hasGlass) cell.checked = false;
    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
    InvalidateRect(&rc, FALSE);
    RebuildTexts();
}
void CCarrierSlotSelector::SetSlotChecked(int portIndex, int slotIndex, BOOL checked)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    if (m_ports[portIndex].allocated) return;
    auto& cell = m_ports[portIndex].slots[slotIndex];
    if (!cell.hasGlass) return;
    cell.checked = !!checked;
    NotifySelectionChanged(portIndex, slotIndex, cell.checked);
    CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
    InvalidateRect(&rc, FALSE);
    RebuildTexts();
}
BOOL CCarrierSlotSelector::GetSlotChecked(int portIndex, int slotIndex) const
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return FALSE;
    return m_ports[portIndex].slots[slotIndex].checked ? TRUE : FALSE;
}
int CCarrierSlotSelector::GetSlotMaterialType(int portIndex, int slotIndex) const
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return MAT_G1;
    return m_ports[portIndex].slots[slotIndex].material;
}
void CCarrierSlotSelector::SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify)
{
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return;
    if (m_ports[portIndex].allocated) return;
    auto& cell = m_ports[portIndex].slots[slotIndex];
    if (!cell.hasGlass) return;
    int mt = (material == MAT_G2) ? MAT_G2 : MAT_G1;
    if (cell.material != mt)
    {
        cell.material = mt;
        if (bNotify) NotifyMaterialChanged(portIndex, slotIndex, cell.material);
        CRect rc; GetSubItemRect(slotIndex, portIndex + 1, LVIR_BOUNDS, rc);
        InvalidateRect(&rc, FALSE);
    }
}
CString CCarrierSlotSelector::GetDisplayId(int portIndex, int slotIndex) const
{
    CString s(_T("—"));
    if (!SAFE_PORT(portIndex) || !SAFE_SLOT(slotIndex)) return s;
    const auto& cell = m_ports[portIndex].slots[slotIndex];
    if (!cell.hasGlass) return s;
    s.Format(_T("G%d-%s"), (cell.material == MAT_G2) ? 2 : 1, cell.coreId.GetString());
    return s;
}
void CCarrierSlotSelector::CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify)
{
    if (!SAFE_PORT(portIndex)) return;
    if (m_ports[portIndex].allocated) return;
    for (int r = 0; r < m_nSlots; ++r)
    {
        auto& cell = m_ports[portIndex].slots[r];
        if (!cell.hasGlass) continue;
        if (cell.checked != !!checked)
        {
            cell.checked = !!checked;
            if (bNotify) NotifySelectionChanged(portIndex, r, cell.checked);
        }
        CRect rc; GetSubItemRect(r, portIndex + 1, LVIR_BOUNDS, rc);
        InvalidateRect(&rc, FALSE);
    }
    RebuildTexts();
}
BOOL CCarrierSlotSelector::OnEraseBkgnd(CDC* /*pDC*/)
{
    // å…³é”®ï¼šä¸è®©ç³»ç»Ÿæ“¦ç™½èƒŒæ™¯ï¼›ç”¨è‡ªç»˜åœ¨ PREPAINT é˜¶æ®µç»Ÿä¸€é“ºåº•
    return TRUE;
}
void CCarrierSlotSelector::OnPaint()
{
    CPaintDC dc(this);
    CRect rc; GetClientRect(&rc);
    CDC memDC;
    memDC.CreateCompatibleDC(&dc);
    CBitmap bmp;
    bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height());
    HGDIOBJ hOldBmp = memDC.SelectObject(bmp);
    // ç»Ÿä¸€åº•色(避免任何擦白)
    memDC.FillSolidRect(rc, m_colBgNorm);
    // è®©æŽ§ä»¶æŠŠå®¢æˆ·åŒºå†…容“画到”内存DC(会触发 NM_CUSTOMDRAW,走你现有自绘逻辑)
    // PRF_ERASEBKGND è®©å†…部如果想擦背景,也在内存里擦,不会闪屏
    SendMessage(WM_PRINTCLIENT,
        reinterpret_cast<WPARAM>(memDC.m_hDC),
        PRF_CLIENT | PRF_ERASEBKGND | PRF_CHILDREN | PRF_OWNED);
    // å›žæ˜¾åˆ°å±å¹•
    dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY);
    memDC.SelectObject(hOldBmp);
    // bmp, memDC æžæž„自动释放
}
void CCarrierSlotSelector::OnSize(UINT nType, int cx, int cy)
{
    CListCtrl::OnSize(nType, cx, cy);
}
CRect CCarrierSlotSelector::GetCheckboxRect(const CRect& cell) const
{
    int sz = max(14, min(int(m_rowHeight * 0.70), 20));
    int leftPad = 8;
    int top = cell.top + (m_rowHeight - sz) / 2;
    return CRect(cell.left + leftPad, top, cell.left + leftPad + sz, top + sz);
}
CRect CCarrierSlotSelector::GetMaterialTagRect(const CRect& cell) const
{
    int tagH = max(14, min(int(m_rowHeight * 0.65), m_rowHeight - 8));
    int tagW = 32;
    int rightPadForDot = 16;
    int gap = 6;
    int top = cell.top + (m_rowHeight - tagH) / 2;
    int right = cell.right - rightPadForDot - gap;
    return CRect(right - tagW, top, right, top + tagH);
}
CRect CCarrierSlotSelector::GetStatusDotRect(const CRect& cell) const
{
    int d = max(8, min(int(m_rowHeight * 0.42), 12));
    int rightPad = 6;
    int top = cell.top + (m_rowHeight - d) / 2;
    return CRect(cell.right - rightPad - d, top, cell.right - rightPad, top + d);
}
void CCarrierSlotSelector::DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled)
{
    CBrush br(disabled ? RGB(245, 245, 245) : RGB(255, 255, 255));
    CPen   pen(PS_SOLID, 1, disabled ? RGB(200, 200, 200) : RGB(130, 130, 135));
    CBrush* pOldB = pDC->SelectObject(&br);
    CPen* pOldP = pDC->SelectObject(&pen);
    pDC->RoundRect(r, CPoint(3, 3));
    pDC->SelectObject(pOldB);
    pDC->SelectObject(pOldP);
    br.DeleteObject(); pen.DeleteObject();
    if (!checked) return;
    COLORREF c = disabled ? RGB(160, 160, 160) : RGB(40, 150, 90);
    CPen penTick(PS_SOLID, max(2, r.Height() / 8), c);
    CPen* pOld = pDC->SelectObject(&penTick);
    POINT p1 = { r.left + r.Width() * 2 / 9, r.top + r.Height() * 5 / 9 };
    POINT p2 = { r.left + r.Width() * 4 / 9, r.top + r.Height() * 7 / 9 };
    POINT p3 = { r.left + r.Width() * 7 / 9, r.top + r.Height() * 3 / 9 };
    pDC->MoveTo(p1); pDC->LineTo(p2); pDC->LineTo(p3);
    pDC->SelectObject(pOld);
    penTick.DeleteObject();
}
void CCarrierSlotSelector::OnLButtonDown(UINT nFlags, CPoint point)
{
    LVHITTESTINFO ht = { 0 }; ht.pt = point;
    int row = SubItemHitTest(&ht);
    int sub = (row >= 0) ? ht.iSubItem : -1;
    if (row >= 0 && sub >= 1)
    {
        int port = sub - 1;
        if (SAFE_PORT(port) && SAFE_SLOT(row))
        {
            auto& pc = m_ports[port];
            auto& cell = pc.slots[row];
            CRect rcCell; GetSubItemRect(row, sub, LVIR_BOUNDS, rcCell);
            if (pc.allocated || !cell.hasGlass)
            {
                CListCtrl::OnLButtonDown(nFlags, point);
                return;
            }
            if (GetCheckboxRect(rcCell).PtInRect(point))
            {
                cell.checked = !cell.checked;
                NotifySelectionChanged(port, row, cell.checked);
                InvalidateRect(&rcCell, FALSE);
                RebuildTexts();
                return;
            }
            if (m_bShowMatToggle && GetMaterialTagRect(rcCell).PtInRect(point))
            {
                cell.material = (cell.material == MAT_G1) ? MAT_G2 : MAT_G1;
                NotifyMaterialChanged(port, row, cell.material);
                InvalidateRect(&rcCell, FALSE);
                return;
            }
        }
    }
    CListCtrl::OnLButtonDown(nFlags, point);
}
void CCarrierSlotSelector::OnMouseMove(UINT nFlags, CPoint point)
{
    CListCtrl::OnMouseMove(nFlags, point);
}
void CCarrierSlotSelector::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
    NMLVCUSTOMDRAW* pCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR);
    switch (pCD->nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
    {
        // å…³é”®ï¼šæ•´æŽ§ä»¶é¢„先铺底,避免初次 hover/显示时“刷白”
        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
        CRect rcClient; GetClientRect(&rcClient);
        pDC->FillSolidRect(rcClient, m_colBgNorm);
        *pResult = CDRF_NOTIFYITEMDRAW;
        return;
    }
    case CDDS_ITEMPREPAINT:
        *pResult = CDRF_NOTIFYSUBITEMDRAW;
        return;
    case (CDDS_SUBITEM | CDDS_ITEMPREPAINT):
    {
        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
        int row = (int)pCD->nmcd.dwItemSpec;
        int sub = pCD->iSubItem;
        CRect rc; GetSubItemRect(row, sub, LVIR_BOUNDS, rc);
        // èƒŒæ™¯
        COLORREF bk = m_colBgNorm;
        if (sub >= 1)
        {
            int port = sub - 1;
            if (port % 2 == 0) bk = m_colBgAlt;             // äº¤æ›¿åˆ—
            if (SAFE_PORT(port) && m_ports[port].allocated) // é”å®šåˆ—
                bk = m_colLockBg;
        }
        pDC->FillSolidRect(rc, bk);
        // ç½‘格(上/左加粗;右/下细线)
        CPen penTopLeft(PS_SOLID, (sub >= 1 && ((sub - 1) % 2 == 0)) ? 2 : 1, RGB(210, 214, 220));
        CPen* pOldPen = pDC->SelectObject(&penTopLeft);
        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.right, rc.top);
        pDC->MoveTo(rc.left, rc.top);   pDC->LineTo(rc.left, rc.bottom);
        pDC->SelectObject(pOldPen); penTopLeft.DeleteObject();
        CPen penThin(PS_SOLID, 1, RGB(220, 224, 230));
        pOldPen = pDC->SelectObject(&penThin);
        if (sub == GetHeaderCtrl()->GetItemCount() - 1) { pDC->MoveTo(rc.right - 1, rc.top); pDC->LineTo(rc.right - 1, rc.bottom); }
        if (row == m_nSlots - 1) { pDC->MoveTo(rc.left, rc.bottom - 1); pDC->LineTo(rc.right, rc.bottom - 1); }
        pDC->SelectObject(pOldPen); penThin.DeleteObject();
        if (sub == 0)
        {
            CString s; s.Format(_T("Slot %d"), row + 1);
            CFont* pOld = pDC->SelectObject(&m_fntBold);
            pDC->SetBkMode(TRANSPARENT);
            pDC->SetTextColor(RGB(60, 60, 64));
            CRect rText = rc; rText.DeflateRect(8, 0, 8, 0);
            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
            pDC->SelectObject(pOld);
        }
        else if (SAFE_PORT(sub - 1) && SAFE_SLOT(row))
        {
            int port = sub - 1;
            const auto& pc = m_ports[port];
            const auto& cell = pc.slots[row];
            // æ‰å¹³å¤é€‰æ¡†
            CRect rChk = GetCheckboxRect(rc);
            DrawFlatCheckbox(pDC, rChk, cell.checked, pc.allocated || !cell.hasGlass);
            // æ–‡æœ¬
            CString s = GetDisplayId(port, row);
            CRect rText = rc;
            int leftPad = rChk.right + 6;
            CRect rDot = GetStatusDotRect(rc);
            CRect rTag = GetMaterialTagRect(rc);
            int rightPad = rc.right - min(rTag.left - 6, rDot.left - 6);
            rText.DeflateRect(leftPad - rc.left, 0, rightPad, 0);
            CFont* pOld = pDC->SelectObject(&m_fntText);
            pDC->SetBkMode(TRANSPARENT);
            pDC->SetTextColor(cell.hasGlass ? RGB(40, 40, 40) : RGB(150, 150, 150));
            pDC->DrawText(s, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
            pDC->SelectObject(pOld);
            // ç‰©æ–™æ ‡ç­¾ï¼ˆå¯éšè—ï¼‰
            if (m_bShowMatToggle)
            {
                CRect rT = rTag;
                COLORREF crBorder = (cell.material == MAT_G2) ? RGB(180, 150, 220) : RGB(120, 160, 220);
                COLORREF crFill = (cell.material == MAT_G2) ? RGB(243, 235, 250) : RGB(233, 240, 252);
                COLORREF crText = (cell.material == MAT_G2) ? RGB(90, 60, 150) : RGB(50, 90, 160);
                if (pc.allocated || !cell.hasGlass) { crBorder = RGB(210, 210, 210); crFill = RGB(245, 245, 245); crText = RGB(160, 160, 160); }
                CBrush br(crFill); CPen tagPen(PS_SOLID, 1, crBorder);
                CPen* pOldP = pDC->SelectObject(&tagPen);
                CBrush* pOldB = pDC->SelectObject(&br);
                pDC->RoundRect(rT, CPoint(6, 6));
                pDC->SelectObject(pOldB); pDC->SelectObject(pOldP);
                br.DeleteObject(); tagPen.DeleteObject();
                CString t; t.Format(_T("G%d"), (cell.material == MAT_G2) ? 2 : 1);
                CFont* pOldS = pDC->SelectObject(&m_fntSmall);
                pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(crText);
                pDC->DrawText(t, rT, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                pDC->SelectObject(pOldS);
            }
            // çŠ¶æ€ç‚¹
            CRect rD = GetStatusDotRect(rc);
            COLORREF dot = cell.hasGlass ? (pc.allocated ? RGB(215, 160, 60) : RGB(60, 170, 80)) : RGB(160, 160, 160);
            CBrush bDot(dot); CBrush* pOldB = pDC->SelectObject(&bDot);
            pDC->Ellipse(rD);
            pDC->SelectObject(pOldB); bDot.DeleteObject();
        }
        *pResult = CDRF_SKIPDEFAULT; // å­é¡¹å®Œå…¨è‡ªç»˜
        return;
    }
    default:
        break;
    }
    *pResult = 0;
}
BOOL CCarrierSlotSelector::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
    NMHDR* pNM = reinterpret_cast<NMHDR*>(lParam);
    if (pNM && pNM->hwndFrom == GetHeaderCtrl()->GetSafeHwnd())
    {
        switch (pNM->code)
        {
        case HDN_ITEMCLICKA:
        case HDN_ITEMCLICKW:
        {
            HD_NOTIFY* phdn = reinterpret_cast<HD_NOTIFY*>(lParam);
            OnHeaderClick(phdn->iItem);
            if (pResult) *pResult = 0;
            return TRUE; // æˆ‘们已处理
        }
        default: break;
        }
    }
    return CListCtrl::OnNotify(wParam, lParam, pResult);
}
void CCarrierSlotSelector::OnHeaderClick(int iItem)
{
    // iItem: 0=Slot åˆ—,>=1 ä¸º Port åˆ—
    if (iItem <= 0) return;
    int port = iItem - 1;
    if (!SAFE_PORT(port) || m_ports[port].allocated) return;
    // è®¡ç®—是否“全选”
    bool anyCheckable = false;
    bool allChecked = true;
    for (int r = 0; r < m_nSlots; ++r)
    {
        const auto& cell = m_ports[port].slots[r];
        if (!cell.hasGlass) continue;
        anyCheckable = true;
        if (!cell.checked) { allChecked = false; break; }
    }
    if (!anyCheckable) return;
    // åˆ‡æ¢ï¼šè‹¥å·²å…¨é€‰ -> å–消全选;否则 -> å…¨é€‰
    CheckAllInPort(port, allChecked ? FALSE : TRUE, TRUE);
}
// ==== Í¨Öª ====
void CCarrierSlotSelector::NotifySelectionChanged(int /*port*/, int /*slot*/, BOOL /*checked*/)
{
    if (GetParent())
    {
        const int code = 0x2001; // CSSN_SELECTION_CHANGED
        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
    }
}
void CCarrierSlotSelector::NotifyMaterialChanged(int /*port*/, int /*slot*/, int /*material*/)
{
    if (GetParent())
    {
        const int code = 0x2002; // CSSN_MATERIAL_CHANGED
        GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
    }
}
SourceCode/Bond/Servo/CCarrierSlotSelector.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
#include "stdafx.h"
#pragma once
#include <vector>
#include <string>
#ifndef _AFX
#include <afxwin.h>
#include <afxcmn.h>
#endif
class CCarrierSlotSelector : public CListCtrl
{
public:
    enum MaterialType { MAT_G1 = 1, MAT_G2 = 2 };
    struct SlotCell
    {
        bool    hasGlass = false;   // æ˜¯å¦æœ‰ç‰‡
        CString coreId;             // å›ºå®šæ ¸å¿ƒID(不含前缀)
        int     material = MAT_G1;  // ä»…影响展示(G1/G2)
        bool    checked = false;   // æ˜¯å¦å‹¾é€‰åŠ å·¥
    };
    struct PortColumn
    {
        CString               portName;     // "Port 1" ...
        CString               carrierName;  // "Carrier A" ...
        bool                  allocated = false; // æ•´åˆ—锁定
        CString               allocatedBy;       // å ç”¨è€…
        std::vector<SlotCell> slots;             // size = m_nSlots
    };
public:
    CCarrierSlotSelector();
    virtual ~CCarrierSlotSelector();
    // åˆå§‹åŒ– / å°ºå¯¸
    void    InitGrid(int nPorts, int nSlots);
    void    SetColumnWidths(int slotColWidth, int portColWidth);
    void    SetRowHeight(int cy); // small image list æŽ§åˆ¶è¡Œé«˜
    // å¤–è§‚
    void    SetShowMaterialToggle(BOOL bShow);
    BOOL    GetShowMaterialToggle() const { return m_bShowMatToggle; }
    // Port æŽ¥å£
    int     GetPortCount() const { return (int)m_ports.size(); }
    int     GetSlotCount() const { return m_nSlots; }
    void    SetPortInfo(int portIndex, LPCTSTR portName, LPCTSTR carrierName);
    void    SetPortAllocated(int portIndex, BOOL allocated, LPCTSTR byName = nullptr);
    BOOL    IsPortAllocated(int portIndex) const;
    // Slot æŽ¥å£
    void    SetSlotGlass(int portIndex, int slotIndex, BOOL hasGlass, LPCTSTR coreId /*可空*/, int material /*1=G1,2=G2*/);
    void    SetSlotChecked(int portIndex, int slotIndex, BOOL checked);
    BOOL    GetSlotChecked(int portIndex, int slotIndex) const;
    int     GetSlotMaterialType(int portIndex, int slotIndex) const; // 1/2
    void    SetSlotMaterialType(int portIndex, int slotIndex, int material, BOOL bNotify = TRUE);
    CString GetDisplayId(int portIndex, int slotIndex) const; // "Gx-core" æˆ– "—"
    void    RebuildTexts(); // åˆ—头计数、单元格临时文本
    // å·¥å…·ï¼šæ•´åˆ—全选/全不选(只影响 hasGlass==true çš„æ ¼å­ï¼‰
    void    CheckAllInPort(int portIndex, BOOL checked, BOOL bNotify = TRUE);
protected:
    // å†…部数据
    BOOL                    m_bFirstShown = FALSE; // å­é¡µé¢é¦–次显示后强制一次全量重绘
    int                     m_nSlots = 0;
    std::vector<PortColumn> m_ports;
    BOOL                    m_bShowMatToggle = TRUE;
    // UI metrics
    int     m_rowHeight = 24;
    int     m_slotColWidth = 100;
    int     m_portColWidth = 180;
    // è¡Œé«˜å›¾åƒåˆ—表
    CImageList m_ilRowHeight;
    // é¢œè‰²
    COLORREF m_colBgAlt = RGB(240, 242, 245); // äº¤æ›¿åˆ—浅灰
    COLORREF m_colBgNorm = RGB(255, 255, 255);
    COLORREF m_colLockBg = RGB(255, 244, 214); // é”å®šåˆ—淡黄
    // å­—体
    CFont   m_fntText;
    CFont   m_fntBold;
    CFont   m_fntSmall;
    // åŒºåŸŸè®¡ç®—
    CRect   GetCheckboxRect(const CRect& cell) const;
    CRect   GetMaterialTagRect(const CRect& cell) const;
    CRect   GetStatusDotRect(const CRect& cell) const;
    // å·¥å…·
    void    EnsureFonts();
    void    UpdateRowCount();
    void    DrawFlatCheckbox(CDC* pDC, const CRect& r, bool checked, bool disabled); // æ‰å¹³å¤é€‰æ¡†
    // é€šçŸ¥çˆ¶çª—口(WM_COMMAND é£Žæ ¼ï¼‰
    void    NotifySelectionChanged(int port, int slot, BOOL checked);
    void    NotifyMaterialChanged(int port, int slot, int material);
protected:
    // MFC
    virtual void PreSubclassWindow() override;
    afx_msg void OnShowWindow(BOOL bShow, UINT nStatus);
    afx_msg void OnWindowPosChanged(WINDOWPOS* lpwndpos);
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg void OnPaint();                // â˜… æ–°å¢žï¼šè‡ªå¸¦åŒç¼“冲绘制
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
    afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); // æ•获 Header é€šçŸ¥
    void    OnHeaderClick(int iItem);
    DECLARE_MESSAGE_MAP()
};
SourceCode/Bond/Servo/CCjPage1.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
// CPjPage1.cpp: å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
#include "Servo.h"
#include "CCjPage1.h"
#include "afxdialogex.h"
// CPjPage1 å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CCjPage1, CCjPageBase)
CCjPage1::CCjPage1(CWnd* pParent /*=nullptr*/)
    : CCjPageBase(IDD_CJ_PAGE1, pParent)
{
}
CCjPage1::~CCjPage1()
{
}
void CCjPage1::DoDataExchange(CDataExchange* pDX)
{
    CCjPageBase::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CCjPage1, CCjPageBase)
    ON_WM_DESTROY()
END_MESSAGE_MAP()
// CPjPage1 æ¶ˆæ¯å¤„理程序
BOOL CCjPage1::OnInitDialog()
{
    CCjPageBase::OnInitDialog();
    return TRUE;  // return TRUE unless you set the focus to a control
                  // å¼‚常: OCX å±žæ€§é¡µåº”返回 FALSE
}
void CCjPage1::OnDestroy()
{
    CCjPageBase::OnDestroy();
    // TODO: åœ¨æ­¤å¤„添加消息处理程序代码
}
void CCjPage1::Resize()
{
    CCjPageBase::Resize();
    /*
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    pItem = GetDlgItem(IDC_LABEL_NO_SEL);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow((rcClient.Width() - rcItem.Width()) / 2,
        (rcClient.Height() - rcItem.Height()) / 2,
        rcItem.Width(), rcItem.Height());
        */
}
SourceCode/Bond/Servo/CCjPage1.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
#pragma once
#include "CCjPageBase.h"
// CPjPage1 å¯¹è¯æ¡†
class CCjPage1 : public CCjPageBase
{
    DECLARE_DYNAMIC(CCjPage1)
public:
    CCjPage1(CWnd* pParent = nullptr);   // æ ‡å‡†æž„造函数
    virtual ~CCjPage1();
protected:
    virtual void Resize();
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_CJ_PAGE1 };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnDestroy();
};
SourceCode/Bond/Servo/CCjPage2.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,480 @@
// CPjPage1.cpp: å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
#include "Servo.h"
#include "CCjPage2.h"
#include "afxdialogex.h"
#include "RecipeManager.h"
UINT btnID[] = { IDC_BUTTON_PORT1_PROCESS_START,
    IDC_BUTTON_PORT2_PROCESS_START,
    IDC_BUTTON_PORT3_PROCESS_START,
    IDC_BUTTON_PORT4_PROCESS_START };
// CPjPage1 å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CCjPage2, CCjPageBase)
CCjPage2::CCjPage2(CWnd* pParent /*=nullptr*/)
    : CCjPageBase(IDD_CJ_PAGE2, pParent)
{
    m_nSelRadioId = 0;
}
CCjPage2::~CCjPage2()
{
}
void CCjPage2::DoDataExchange(CDataExchange* pDX)
{
    CCjPageBase::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CCjPage2, CCjPageBase)
    ON_WM_DESTROY()
    ON_EN_CHANGE(IDC_EDIT_PJ_ID, &CCjPage2::OnEnChangeEditPjId)
    ON_CBN_SELCHANGE(IDC_COMBO_RECIPE, &CCjPage2::OnCbnSelchangeComboRecipe)
    ON_BN_CLICKED(IDC_RADIO1, &CCjPage2::OnBnClickedRadio1)
    ON_BN_CLICKED(IDC_RADIO2, &CCjPage2::OnBnClickedRadio2)
    ON_BN_CLICKED(IDC_RADIO3, &CCjPage2::OnBnClickedRadio3)
    ON_BN_CLICKED(IDC_RADIO4, &CCjPage2::OnBnClickedRadio4)
    ON_NOTIFY(CSGN_SEL_CHANGED, IDC_GRID1, &CCjPage2::OnGridSelChanged)
    ON_NOTIFY(CSGN_MAT_CHANGED, IDC_GRID1, &CCjPage2::OnGridMatChanged)
    ON_BN_CLICKED(IDC_BUTTON_PORT1_PROCESS_START, &CCjPage2::OnBnClickedButtonPort1ProcessStart)
    ON_BN_CLICKED(IDC_BUTTON_PORT2_PROCESS_START, &CCjPage2::OnBnClickedButtonPort2ProcessStart)
    ON_BN_CLICKED(IDC_BUTTON_PORT3_PROCESS_START, &CCjPage2::OnBnClickedButtonPort3ProcessStart)
    ON_BN_CLICKED(IDC_BUTTON_PORT4_PROCESS_START, &CCjPage2::OnBnClickedButtonPort4ProcessStart)
END_MESSAGE_MAP()
// CPjPage1 æ¶ˆæ¯å¤„理程序
void CCjPage2::OnSetContext(void* pContext)
{
    UpdatePjData();
}
void CCjPage2::SetPjWarps(std::vector<PJWarp>& pjs)
{
    m_pjWarps = pjs;
}
BOOL CCjPage2::OnInitDialog()
{
    CCjPageBase::OnInitDialog();
    m_grid.SubclassDlgItem(IDC_GRID1, this);
     m_grid.InitGrid(4, 8);
    m_grid.SetColumnWidths(100, 220);
    m_grid.SetRowHeight(32);
    m_grid.SetHeaderHeight(36);
    m_grid.EnableColumnResize(FALSE); // ç¦æ­¢æ‹–动列宽
    m_grid.SetShowMaterialToggle(TRUE);
    m_grid.DisableSystemScrollbars();
    m_grid.ResizeWindowToFitAll(TRUE); // TRUE=包含非客户区(边框、标题栏)
    m_grid.SetNoScrollbarsMode(TRUE);           // å½»åº•禁用滚动条
    m_grid.FitWindowToContentNoScroll(TRUE);    // çª—口尺寸刚好容纳全部内容(不出现滚动条)
    m_grid.SetPortInfo(0, _T("Port 1"), _T(""));
    m_grid.SetPortInfo(1, _T("Port 2"), _T(""));
    m_grid.SetPortInfo(2, _T("Port 3"), _T(""));
    m_grid.SetPortInfo(3, _T("Port 4"), _T(""));
    UpdatePjData();
    return TRUE;  // return TRUE unless you set the focus to a control
                  // å¼‚常: OCX å±žæ€§é¡µåº”返回 FALSE
}
void CCjPage2::OnDestroy()
{
    CCjPageBase::OnDestroy();
    // TODO: åœ¨æ­¤å¤„添加消息处理程序代码
}
void CCjPage2::Resize()
{
    CCjPageBase::Resize();
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    pItem = GetDlgItem(IDC_GRID1);
    pItem->GetWindowRect(&rcItem);
    ScreenToClient(rcItem);
    int x = rcItem.left + 100 + 18;
    int y = 100;
    // è®©æŽ§ä»¶çª—口尺寸自动匹配当前列宽/行数(不出现滚动条)
    if (::IsWindow(m_grid.m_hWnd)) {
        CSize best = m_grid.CalcBestWindowSize(TRUE, -1, 2, 2);
        pItem->MoveWindow(rcItem.left, rcItem.top, best.cx, best.cy);
        pItem->Invalidate();
        pItem->GetWindowRect(&rcItem);
        ScreenToClient(rcItem);
        y = rcItem.bottom;
        y += 18;
    }
    pItem = GetDlgItem(IDC_BUTTON_PORT1_PROCESS_START);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
    x += 220;
    pItem = GetDlgItem(IDC_BUTTON_PORT2_PROCESS_START);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
    x += 220;
    pItem = GetDlgItem(IDC_BUTTON_PORT3_PROCESS_START);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
    x += 220;
    pItem = GetDlgItem(IDC_BUTTON_PORT4_PROCESS_START);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x, y, rcItem.Width(), rcItem.Height());
    x += 220;
}
int CCjPage2::OnApply()
{
    //SERVO::CProcessJob*
    if (m_pContext == nullptr) return -1;
    PJWarp* pPjWarp = (PJWarp*)m_pContext;
    SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
    // æ›´æ–°åç§°
    BOOL bOkName = TRUE;
    char szBuffer[256];
    GetDlgItemText(IDC_EDIT_PJ_ID, szBuffer, 256);
    for (auto item : m_pjWarps) {
        if (item.pj != pProcessJob) {
            SERVO::CProcessJob* temp = (SERVO::CProcessJob*)item.pj;
            if (temp->id().compare(std::string(szBuffer)) == 0) {
                bOkName = FALSE;
                break;
            }
        }
    }
    if (!bOkName) {
        AfxMessageBox("不能使用和其它Process Job相同的ID");
        return -1;
    }
    pProcessJob->setId(std::string(szBuffer));
    // æ›´æ–°é…æ–¹
    CString strRecipe;
    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
    int idx = pComboBox->GetCurSel();
    if (idx >= 0) {
        pComboBox->GetLBText(idx, strRecipe);
#ifdef UNICODE
        CT2A utf8Str(strRecipe, CP_UTF8);
        std::string recipe(utf8Str);
#else
        std::string recipe(strRecipe.GetString());
#endif
        pProcessJob->setRecipe(SERVO::RecipeMethod::NoTuning, recipe);
    }
    // æ›´æ–°Port
    int port = -1;
    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4 };
    for (int i = 0; i < 4; i++) {
        int state = ((CButton*)GetDlgItem(ids[i]))->GetCheck();
        if (state == BST_CHECKED) port = i;
    }
    pPjWarp->port = port;
    if (pPjWarp->port != -1) {
        for (int i = 0; i < 8; i++) {
            pPjWarp->checkSlot[i] = m_grid.GetSlotChecked(pPjWarp->port, i);
            pPjWarp->material[i] = m_grid.GetSlotMaterialType(pPjWarp->port, i);
        }
    }
    ContentChanged(1);
    return 0;
}
void CCjPage2::UpdatePjData()
{
    if (m_pContext == nullptr) return;
    m_bContentChangedLock = TRUE;
    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_RECIPE);
    pComboBox->ResetContent();
    std::vector<std::string> vecRecipe = RecipeManager::getInstance().getAllPPID();
    for (const auto& recipe : vecRecipe) {
        pComboBox->AddString(CString(recipe.c_str()));
    }
    // ComboBox
    PJWarp* pPjWarp = (PJWarp*)m_pContext;
    SERVO::CProcessJob* pProcessJob = (SERVO::CProcessJob*)pPjWarp->pj;
    SetDlgItemText(IDC_EDIT_PJ_ID, pProcessJob->id().c_str());
    int idx = pComboBox->FindStringExact(-1, pProcessJob->recipeSpec().c_str());
    if (idx != CB_ERR) pComboBox->SetCurSel(idx);
    // 4个checkbox
    static int ids[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4};
    static char* pszUsed[] = { "Port1(已占用)", "Port2(已占用)", "Port3(已占用)", "Port4(已占用)" };
    static char* pszUnUsed[] = { "Port1(可用)", "Port2(可用)", "Port3(可用)", "Port4(可用)" };
    int portIndex = -1;
    bool enable[] = {true, true, true, true};
    bool checked[] = { false, false, false, false };
    for (auto item : m_pjWarps) {
        if (0 <= item.port && item.port <= 4 && item.pj != ((PJWarp*)m_pContext)->pj) {
            enable[item.port] = false;
        }
    }
    if (0 <= ((PJWarp*)m_pContext)->port && ((PJWarp*)m_pContext)->port <= 3) {
        checked[((PJWarp*)m_pContext)->port] = true;
        portIndex = ((PJWarp*)m_pContext)->port;
        m_nSelRadioId = ids[((PJWarp*)m_pContext)->port];
    }
    for (int i = 0; i < 4; i++) {
        CButton* pButton = (CButton*)GetDlgItem(ids[i]);
        pButton->SetCheck(checked[i] ? BST_CHECKED : BST_UNCHECKED);
        pButton->SetWindowText(enable[i] ? pszUnUsed[i] : pszUsed[i]);
        pButton->EnableWindow(enable[i]);
        m_grid.SetPortAllocated(i, !checked[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(checked[i]);
    }
    // è¯»å–出真实数据
    auto& master = theApp.m_model.getMaster();
    int EQID[] = {EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4};
    for (int p = 0; p < 4; p++) {
        SERVO::CLoadPort* pPort = (SERVO::CLoadPort*)master.getEquipment(EQID[p]);
        m_grid.SetPortInfo(p,
            (std::string("Port ") + std::to_string(p+1)).c_str(),
            pPort->getCassetteId().c_str()
        );
        for (int i = 0; i < SLOT_MAX; ++i) {
            SERVO::CSlot* pSlot = pPort->getSlot(i);
            if (!pSlot) {
                continue;
            }
            // è®¾ç½® Panel ID
            SERVO::CGlass* pGlass = dynamic_cast<SERVO::CGlass*>(pSlot->getContext());
            SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
            if (pGlass != nullptr && pJobDataS != nullptr) {
                m_grid.SetSlotGlass(p, i, TRUE,
                    pGlass->getID().c_str(),
                    m_pjWarps[p].material[i]);
            }
            else {
                m_grid.SetSlotGlass(p, i, FALSE, nullptr, CCarrierSlotGrid::MAT_G1);
            }
        }
    }
    // è®¾ç½®å‹¾é€‰æ•°æ®
    if (portIndex != -1) {
        for (int i = 0; i < 8; i++) {
            m_grid.SetSlotChecked(portIndex, i, ((PJWarp*)m_pContext)->checkSlot[i]);
        }
    }
    m_bContentChangedLock = FALSE;
}
void CCjPage2::OnEnChangeEditPjId()
{
    ContentChanged(0);
}
void CCjPage2::OnCbnSelchangeComboRecipe()
{
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio1()
{
    BOOL lock[] = {TRUE, TRUE, TRUE, TRUE};
    if (IDC_RADIO1 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO1);
        m_nSelRadioId = IDC_RADIO1;
        lock[0] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio2()
{
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO2 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO2);
        m_nSelRadioId = IDC_RADIO2;
        lock[1] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio3()
{
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO3 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO3);
        m_nSelRadioId = IDC_RADIO3;
        lock[2] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
}
void CCjPage2::OnBnClickedRadio4()
{
    BOOL lock[] = { TRUE, TRUE, TRUE, TRUE };
    if (IDC_RADIO4 == m_nSelRadioId) {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, 0);
        m_nSelRadioId = 0;
    }
    else {
        CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO4);
        m_nSelRadioId = IDC_RADIO4;
        lock[3] = FALSE;
    }
    for (int i = 0; i < 4; i++) {
        m_grid.SetPortAllocated(i, lock[i], _T(""));
        GetDlgItem(btnID[i])->EnableWindow(!lock[i]);
    }
    ContentChanged(0);
}
void CCjPage2::OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* nm = reinterpret_cast<CSG_SEL_CHANGE*>(pNMHDR);
    const int port = nm->port;
    const int slot = nm->slot;
    const BOOL chk = nm->checked;
    // è¿™é‡Œå†™ä½ çš„业务逻辑
    // ä¾‹å¦‚:更新状态栏 / åŒæ­¥å…¶å®ƒæŽ§ä»¶ / ç»Ÿè®¡æ•°é‡
    ContentChanged(0);
    /*
    if (m_pContext != nullptr) {
        PJWarp* pjWarp = (PJWarp*)m_pContext;
        for (int i = 0; i < 8; i++) {
            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
        }
    }
    */
    *pResult = 0;
}
void CCjPage2::OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* nm = reinterpret_cast<CSG_MAT_CHANGE*>(pNMHDR);
    const int port = nm->port;
    const int slot = nm->slot;
    const int mat = nm->material; // 1/2
    // ä¾‹å¦‚:即刻刷新右侧预览/记录日志等
    ContentChanged(0);
    /*
    if (m_pContext != nullptr) {
        PJWarp* pjWarp = (PJWarp*)m_pContext;
        for (int i = 0; i < 8; i++) {
            pjWarp->checkSlot[i] = m_grid.GetSlotChecked(port, i);
            pjWarp->material[i] = m_grid.GetSlotMaterialType(port, i);
        }
    }
    */
    *pResult = 0;
}
void CCjPage2::OnBnClickedButtonPort1ProcessStart()
{
    auto& master = theApp.m_model.getMaster();
    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT1);
    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
}
void CCjPage2::OnBnClickedButtonPort2ProcessStart()
{
    auto& master = theApp.m_model.getMaster();
    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT2);
    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
}
void CCjPage2::OnBnClickedButtonPort3ProcessStart()
{
    auto& master = theApp.m_model.getMaster();
    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT3);
    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
}
void CCjPage2::OnBnClickedButtonPort4ProcessStart()
{
    auto& master = theApp.m_model.getMaster();
    auto port = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT4);
    port->sendCassetteCtrlCmd(CCC_PROCESS_START, nullptr, 0, 0, 0, nullptr, nullptr);
}
SourceCode/Bond/Servo/CCjPage2.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
#pragma once
#include "CCjPageBase.h"
#include "ProcessJob.h"
#include "CCarrierSlotGrid.h"
// CPjPage1 å¯¹è¯æ¡†
class CCjPage2 : public CCjPageBase
{
    DECLARE_DYNAMIC(CCjPage2)
public:
    CCjPage2(CWnd* pParent = nullptr);   // æ ‡å‡†æž„造函数
    virtual ~CCjPage2();
public:
    void SetPjWarps(std::vector<PJWarp>& pjs);
protected:
    void Resize();
    virtual int OnApply();
    virtual void OnSetContext(void* pContext);
private:
    void UpdatePjData();
private:
    CCarrierSlotGrid m_grid;
    std::vector<PJWarp> m_pjWarps;
    int m_nSelRadioId;
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_CJ_PAGE1 };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnDestroy();
    afx_msg void OnEnChangeEditPjId();
    afx_msg void OnCbnSelchangeComboRecipe();
    afx_msg void OnBnClickedRadio1();
    afx_msg void OnBnClickedRadio2();
    afx_msg void OnBnClickedRadio3();
    afx_msg void OnBnClickedRadio4();
    afx_msg void OnGridSelChanged(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnGridMatChanged(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnBnClickedButtonPort1ProcessStart();
    afx_msg void OnBnClickedButtonPort2ProcessStart();
    afx_msg void OnBnClickedButtonPort3ProcessStart();
    afx_msg void OnBnClickedButtonPort4ProcessStart();
};
SourceCode/Bond/Servo/CCjPage3.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
// CPjPage1.cpp: å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
#include "Servo.h"
#include "CCjPage3.h"
#include "afxdialogex.h"
// CPjPage1 å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CCjPage3, CCjPageBase)
CCjPage3::CCjPage3(CWnd* pParent /*=nullptr*/)
    : CCjPageBase(IDD_CJ_PAGE3, pParent)
{
}
CCjPage3::~CCjPage3()
{
}
void CCjPage3::DoDataExchange(CDataExchange* pDX)
{
    CCjPageBase::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CCjPage3, CCjPageBase)
    ON_WM_DESTROY()
    ON_EN_CHANGE(IDC_EDIT_CJ_ID, &CCjPage3::OnEnChangeEditCjId)
END_MESSAGE_MAP()
// CPjPage1 æ¶ˆæ¯å¤„理程序
BOOL CCjPage3::OnInitDialog()
{
    CCjPageBase::OnInitDialog();
    return TRUE;  // return TRUE unless you set the focus to a control
                  // å¼‚常: OCX å±žæ€§é¡µåº”返回 FALSE
}
void CCjPage3::OnDestroy()
{
    CCjPageBase::OnDestroy();
    // TODO: åœ¨æ­¤å¤„添加消息处理程序代码
}
void CCjPage3::Resize()
{
    CCjPageBase::Resize();
    /*
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    pItem = GetDlgItem(IDC_LABEL_NO_SEL);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow((rcClient.Width() - rcItem.Width()) / 2,
        (rcClient.Height() - rcItem.Height()) / 2,
        rcItem.Width(), rcItem.Height());
        */
}
int CCjPage3::OnApply()
{
    // SERVO::CControlJob*
    if (m_pContext == nullptr) return -1;
    SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_pContext;
    // æ›´æ–°åç§°
    BOOL bOkName = TRUE;
    char szBuffer[256];
    GetDlgItemText(IDC_EDIT_CJ_ID, szBuffer, 256);
    pControlJob->setId(std::string(szBuffer));
    ContentChanged(1);
    return 0;
}
void CCjPage3::OnSetContext(void* pContext)
{
    UpdateCjData();
}
void CCjPage3::UpdateCjData()
{
    if (m_pContext == nullptr) return;
    m_bContentChangedLock = TRUE;
    SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_pContext;
    SetDlgItemText(IDC_EDIT_CJ_ID, pControlJob->id().c_str());
    m_bContentChangedLock = FALSE;
}
void CCjPage3::OnEnChangeEditCjId()
{
    ContentChanged(0);
}
SourceCode/Bond/Servo/CCjPage3.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
#pragma once
#include "CCjPageBase.h"
#include "CControlJob.h"
// CPjPage1 å¯¹è¯æ¡†
class CCjPage3 : public CCjPageBase
{
    DECLARE_DYNAMIC(CCjPage3)
public:
    CCjPage3(CWnd* pParent = nullptr);   // æ ‡å‡†æž„造函数
    virtual ~CCjPage3();
protected:
    void Resize();
    virtual int OnApply();
    virtual void OnSetContext(void* pContext);
    void UpdateCjData();
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_CJ_PAGE1 };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnDestroy();
    afx_msg void OnEnChangeEditCjId();
};
SourceCode/Bond/Servo/CCjPageBase.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
// CPjPage1.cpp: å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
#include "Servo.h"
#include "CCjPageBase.h"
#include "afxdialogex.h"
// CPjPage1 å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CCjPageBase, CDialogEx)
CCjPageBase::CCjPageBase(UINT nID, CWnd* pPage) : CDialogEx(nID, pPage)
{
    m_crBkgnd = RGB(255, 255, 255);
    m_crBkgndCached = CLR_INVALID;
    m_onContentChanged = nullptr;
    m_bContentChangedLock = FALSE;
    m_pContext = nullptr;
    m_nContextType = 0;
}
CCjPageBase::~CCjPageBase()
{
}
void CCjPageBase::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CCjPageBase, CDialogEx)
    ON_WM_CTLCOLOR()
    ON_WM_SIZE()
END_MESSAGE_MAP()
// CPjPage1 æ¶ˆæ¯å¤„理程序
void CCjPageBase::SetTitle(CString strTitle)
{
    SetDlgItemText(IDC_LABEL_TITLE, strTitle);
}
void CCjPageBase::SetContext(void* pContext, int type)
{
    m_pContext = pContext;
    m_nContextType = type;
    OnSetContext(pContext);
}
void* CCjPageBase::GetContext()
{
    return m_pContext;
}
void CCjPageBase::SetOnContentChanged(ONCONTENTCHANGED onContentChanged)
{
    m_onContentChanged = onContentChanged;
}
BOOL CCjPageBase::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    Resize();
    m_labelTitle.SubclassDlgItem(IDC_LABEL_TITLE, this);
    m_labelTitle.Setpadding(PADDING_LEFT, 0);
    m_labelTitle.Setpadding(PADDING_RIGHT, 0);
 ;
    return TRUE;  // return TRUE unless you set the focus to a control
                  // å¼‚常: OCX å±žæ€§é¡µåº”返回 FALSE
}
HBRUSH CCjPageBase::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
    // æƒ³ç»™å“ªäº›æŽ§ä»¶æ”¹åº•色就把它们的类型写进来:
    const bool needCustomBg =
        (nCtlColor == CTLCOLOR_STATIC) ||
        (nCtlColor == CTLCOLOR_DLG) ||   // å¯¹è¯æ¡†åº•色
        (nCtlColor == CTLCOLOR_BTN);        // æŒ‰é’®ï¼ˆå¯é€‰ï¼‰
    if (needCustomBg)
    {
        // è‹¥ç¬¬ä¸€æ¬¡åˆ›å»ºï¼Œæˆ–颜色改变则重建
        if (m_brBkgnd.GetSafeHandle() == nullptr || m_crBkgndCached != m_crBkgnd)
        {
            if (m_brBkgnd.GetSafeHandle())
                m_brBkgnd.DeleteObject();
            m_brBkgnd.CreateSolidBrush(m_crBkgnd);
            m_crBkgndCached = m_crBkgnd;
        }
        // æ–‡æœ¬å‰æ™¯/背景设置(仅影响文本绘制)
        pDC->SetBkColor(m_crBkgnd);
        pDC->SetTextColor(RGB(0, 0, 0));
        // å¦‚需让静态文本透明叠在底色上,可用:
        // pDC->SetBkMode(TRANSPARENT);
        return (HBRUSH)m_brBkgnd; // å®‰å…¨çš„隐式转换
    }
    // å…¶ä»–控件类型沿用基类默认的刷子
    return hbr;
}
void CCjPageBase::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);
    if (GetDlgItem(IDC_LABEL_TITLE) == nullptr) return;
    Resize();
}
void CCjPageBase::Resize()
{
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    pItem = GetDlgItem(IDC_LABEL_TITLE);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(12, 8, rcClient.Width() - 24, rcItem.Height());
}
void CCjPageBase::ContentChanged(int code)
{
    if (!m_bContentChangedLock && m_onContentChanged != nullptr) {
        m_onContentChanged(this, code, m_pContext, m_nContextType);
    }
}
SourceCode/Bond/Servo/CCjPageBase.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
#pragma once
#include "GroupLabel.h"
#include <functional>
typedef std::function<void(void* pFrom, int code, void* pContext, int contextType)> ONCONTENTCHANGED;
// CPjPage1 å¯¹è¯æ¡†
class CCjPageBase : public CDialogEx
{
    DECLARE_DYNAMIC(CCjPageBase)
public:
    CCjPageBase(UINT nID, CWnd* pPage);            // æ ‡å‡†æž„造函数
    virtual ~CCjPageBase();
public:
    void SetTitle(CString strTitle);
    virtual int OnApply() { return 0; };
    void SetOnContentChanged(ONCONTENTCHANGED onContentChanged);
    void SetContext(void* pContext, int type);
    void* GetContext();
protected:
    virtual void Resize();
    virtual void ContentChanged(int code);
    virtual void OnSetContext(void* pContext) { };
private:
    COLORREF m_crBkgndCached;
    CBrush m_brBkgnd;
    CGroupLabel m_labelTitle;
    ONCONTENTCHANGED m_onContentChanged;
protected:
    COLORREF m_crBkgnd;
    BOOL m_bContentChangedLock;
    void* m_pContext;
    int m_nContextType;
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_CJ_PAGE1 };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
    afx_msg void OnSize(UINT nType, int cx, int cy);
};
SourceCode/Bond/Servo/CControlJob.cpp
@@ -4,9 +4,12 @@
#include "SerializeUtil.h"
static inline std::string trimCopy(std::string s) {
    auto notspace = [](int ch) { return !std::isspace(ch); };
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
    s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
    s.erase(s.begin(),
        std::find_if(s.begin(), s.end(),
            [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }));
    s.erase(std::find_if(s.rbegin(), s.rend(),
        [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }).base(),
        s.end());
    return s;
}
@@ -35,6 +38,12 @@
        m_tEnd = src.m_tEnd;
    }
    void CControlJob::setId(std::string& id)
    {
        m_cjId = trimCopy(id);
        clampString(m_cjId, MAX_ID_LEN);
    }
    bool CControlJob::addPJ(const std::string& pjId) {
        if (pjId.empty()) return false;
        auto id = pjId;
@@ -57,6 +66,28 @@
        return true;
    }
    bool CControlJob::addPjPointer(CProcessJob* pj)
    {
        for (auto item : m_pjs) {
            if (item->id().compare(pj->id()) == 0) return false;
        }
        m_pjs.push_back(pj);
        return true;
    }
    bool CControlJob::removePjPointer(const std::string& id)
    {
        for(auto iter = m_pjs.begin(); iter != m_pjs.end(); ++iter) {
            if ((*iter)->id().compare(id) == 0) {
                m_pjs.erase(iter);
                return true;
            }
        }
        return false;
    }
    bool CControlJob::removePJ(const std::string& pjId) {
        auto it = std::find(m_pjIds.begin(), m_pjIds.end(), pjId);
        if (it == m_pjIds.end()) return false;
@@ -71,6 +102,11 @@
    const std::vector<CControlJob::ValidationIssue>& CControlJob::issues()
    {
        return m_issues;
    }
    void CControlJob::clearIssues()
    {
        m_issues.clear();
    }
    bool CControlJob::validateForCreate(
@@ -184,9 +220,11 @@
        return true;
    }
    bool CControlJob::abort() {
    bool CControlJob::abort(std::string reason) {
        if (m_state == CJState::Completed || m_state == CJState::Aborted || m_state == CJState::Failed)
            return false;
        m_failReason = trimCopy(reason);
        clampString(m_failReason, 128);
        m_state = CJState::Aborted;
        markEnd();
        return true;
@@ -281,23 +319,12 @@
        out = CControlJob(cjId);
        out.setPriority(prio);
        // ç›´æŽ¥æ¢å¤å†…部状态(若你要求走状态机,可在这里按合法过渡调用 queue()/start()/...)
        // ç®€åŒ–:直接赋值(你在 CControlJob.cpp å†…部,可访问私有成员)
        struct Access : CControlJob {
            using CControlJob::m_state;
            using CControlJob::m_failReason;
            using CControlJob::m_tQueued;
            using CControlJob::m_tStart;
            using CControlJob::m_tEnd;
            using CControlJob::m_pjIds;
        };
        auto& a = reinterpret_cast<Access&>(out);
        a.m_state = static_cast<CJState>(st);
        a.m_failReason = std::move(failText);
        a.m_tQueued = std::move(tQ);
        a.m_tStart = std::move(tS);
        a.m_tEnd = std::move(tE);
        a.m_pjIds = std::move(pjIds);
        out.m_state = static_cast<CJState>(st);
        out.m_failReason = std::move(failText);
        out.m_tQueued = std::move(tQ);
        out.m_tStart = std::move(tS);
        out.m_tEnd = std::move(tE);
        out.m_pjIds = std::move(pjIds);
        return true;
    }
SourceCode/Bond/Servo/CControlJob.h
@@ -42,6 +42,7 @@
        // â€”— åŸºæœ¬å±žæ€§ â€”— //
        const std::string& id()     const noexcept { return m_cjId; }
        void setId(std::string& id);
        CJState            state()  const noexcept { return m_state; }
        uint8_t            priority() const noexcept { return m_priority; }
        void               setPriority(uint8_t p) noexcept { m_priority = p; }
@@ -54,6 +55,8 @@
        bool containsPJ(const std::string& pjId) const;
        const std::vector<std::string>& pjIds() const noexcept { return m_pjIds; }
        bool setPJs(const std::vector<CProcessJob*>& pjs);
        bool removePjPointer(const std::string& id);
        bool addPjPointer(CProcessJob* pj);
        void clearPJs() { m_pjIds.clear(); }
        const std::vector<CProcessJob*>& getPjs() { return m_pjs; };
@@ -68,7 +71,8 @@
            const std::function<bool(const std::string&)>& getPjExistsFn,
            const std::function<bool(const std::string&)>& canJoinFn
        );
        const std::vector<CControlJob::ValidationIssue>& CControlJob::issues();
        const std::vector<ValidationIssue>& CControlJob::issues();
        void clearIssues();
        // â€”— S14F9 â†’ S14F10 çš„“应用结果”模型 â€”— //
        struct CreateRequest {
@@ -95,7 +99,7 @@
        bool pause();          // Executing -> Paused
        bool resume();         // Paused -> Executing
        bool complete();       // Executing/Paused -> Completed
        bool abort();          // éžç»ˆæ€ -> Aborted
        bool abort(std::string reason);          // éžç»ˆæ€ -> Aborted
        bool fail(std::string reason); // ä»»æ„ -> Failed
        const std::string& failReason() const noexcept { return m_failReason; }
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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,652 @@
// CControlJobManagerDlg.cpp: å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
#include "Servo.h"
#include "CControlJobManagerDlg.h"
#include "afxdialogex.h"
#include "ToolUnits.h"
#include "RecipeManager.h"
bool CControlJobManagerDlg::m_bHasState = false;
CControlJobManagerDlg::State CControlJobManagerDlg::m_state{};
// CControlJobManagerDlg å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CControlJobManagerDlg, CDialogEx)
CControlJobManagerDlg::CControlJobManagerDlg(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_DIALOG_CONTROL_JOB_MANAGER, pParent)
{
    m_pControlJob = nullptr;
}
CControlJobManagerDlg::~CControlJobManagerDlg()
{
}
void CControlJobManagerDlg::FreeState()
{
    if (!m_bHasState) return;
    for (auto item : m_state.pjWarps) {
        delete (SERVO::CProcessJob*)item.pj;
    }
    m_state.pjWarps.clear();
    if (m_state.pControlJob != nullptr) {
        delete m_state.pControlJob;
        m_state.pControlJob = nullptr;
    }
    m_bHasState = false;
}
void CControlJobManagerDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_TREE1, m_tree);
}
BEGIN_MESSAGE_MAP(CControlJobManagerDlg, CDialogEx)
    ON_WM_SIZE()
    ON_WM_GETMINMAXINFO()
    ON_NOTIFY(TVN_ITEMCHANGED, IDC_TREE1, &CControlJobManagerDlg::OnTvnItemChangedTree)
    ON_NOTIFY(NM_CLICK, IDC_TREE1, &CControlJobManagerDlg::OnTreeClick)          // æ–°å¢ž
    ON_NOTIFY(TVN_KEYDOWN, IDC_TREE1, &CControlJobManagerDlg::OnTreeKeyDown)        // æ–°å¢ž
    ON_MESSAGE(WM_AFTER_TVCHECK, &CControlJobManagerDlg::OnAfterTvCheck)                // æ–°å¢ž
    ON_WM_DESTROY()
    ON_BN_CLICKED(IDC_BUTTON_APPLY, &CControlJobManagerDlg::OnBnClickedButtonApply)
    ON_NOTIFY(TVN_SELCHANGING, IDC_TREE1, &CControlJobManagerDlg::OnTvnSelchangingTree1)
    ON_BN_CLICKED(IDC_BUTTON_BATH_COMPLETION, &CControlJobManagerDlg::OnBnClickedButtonBathCompletion)
END_MESSAGE_MAP()
// CControlJobManagerDlg æ¶ˆæ¯å¤„理程序
BOOL CControlJobManagerDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    auto onContentChanged = [&](void* pFrom, int code, void* pContext, int contextType) -> void {
        if (0 == code) {
            GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(TRUE);
        }
        else if (1 == code) {
            if (contextType == 1) {
                UpProcessJobId((PJWarp*)pContext);
            }
            else if (contextType == 2) {
                UpControlJobId((SERVO::CControlJob*)pContext);
            }
        }
    };
    // page1
    CCjPage1* pPage1 = new CCjPage1(this);
    pPage1->Create(IDD_CJ_PAGE1, this);
    pPage1->SetTitle(_T("未选择"));
    pPage1->SetOnContentChanged(onContentChanged);
    pPage1->ShowWindow(SW_SHOW);
    m_pages.push_back(pPage1);
    // page2
    CCjPage2* pPage2 = new CCjPage2(this);
    pPage2->Create(IDD_CJ_PAGE2, this);
    pPage2->SetTitle(_T("ProcessJob"));
    pPage2->SetOnContentChanged(onContentChanged);
    m_pages.push_back(pPage2);
    // page3
    CCjPage3* pPage3 = new CCjPage3(this);
    pPage3->Create(IDD_CJ_PAGE3, this);
    pPage3->SetTitle(_T("ControlJob"));
    pPage3->SetOnContentChanged(onContentChanged);
    m_pages.push_back(pPage3);
    // tree
    m_tree.ModifyStyle(0, TVS_CHECKBOXES);
    InitData();
    UpdateControlJob();
    UpdateCtrlState();
    Resize();
    return TRUE;  // return TRUE unless you set the focus to a control
                  // å¼‚常: OCX å±žæ€§é¡µåº”返回 FALSE
}
void CControlJobManagerDlg::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);
    if (GetDlgItem(IDC_TREE1) == nullptr) return;
    Resize();
}
void CControlJobManagerDlg::Resize()
{
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    GetDlgItem(IDCANCEL)->GetWindowRect(&rcItem);
    ScreenToClient(&rcItem);
    const int LEFTWIDTH = 218;
    int x = 12, y = 12;
    int x2 = rcClient.right - 12;
    int y2 = rcClient.bottom - 12;
    // å…ˆç§»åŠ¨æŒ‰é’®
    pItem = GetDlgItem(IDC_BUTTON_APPLY);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x2 - rcItem.Width(), y2 - rcItem.Height(), rcItem.Width(), rcItem.Height());
    x2 -= rcItem.Width();
    x2 -= 8;
    pItem = GetDlgItem(IDC_BUTTON_BATH_COMPLETION);
    pItem->GetWindowRect(&rcItem);
    pItem->MoveWindow(x2 - rcItem.Width(), y2 - rcItem.Height(), rcItem.Width(), rcItem.Height());
    x += rcItem.Width();
    x += 8;
    y2 -= rcItem.Height();
    y2 -= 8;
    // æ ‘控件
    x = 12;
    y = 12;
    pItem = GetDlgItem(IDC_TREE1);
    pItem->MoveWindow(x, y, LEFTWIDTH, y2 - y);
    x += LEFTWIDTH;
    x += 5;
    // å­é¡µé¢
    x2 = rcClient.right - 12;
    for (auto page : m_pages) {
        page->MoveWindow(x, 12, x2 - x, y2 - 12);
    }
}
void CControlJobManagerDlg::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
{
    CDialogEx::OnGetMinMaxInfo(lpMMI);
    // è®¾ç½®æœ€å°å®½é«˜ï¼ˆæ¯”如 400x300)
    lpMMI->ptMinTrackSize.x = 600;
    lpMMI->ptMinTrackSize.y = 400;
    // ä¹Ÿå¯ä»¥é¡ºä¾¿è®¾ç½®æœ€å¤§å®½é«˜
    // lpMMI->ptMaxTrackSize.x = 800;
    // lpMMI->ptMaxTrackSize.y = 600;
}
void CControlJobManagerDlg::UpdateCtrlState()
{
    GetDlgItem(IDC_BUTTON_BATH_COMPLETION)->EnableWindow(true);
}
void CControlJobManagerDlg::UpdateControlJob()
{
    m_tree.DeleteAllItems();
    if (m_pControlJob == nullptr) return;
    HTREEITEM hRoot = m_tree.InsertItem(m_pControlJob->id().c_str(), 0, 0);
    m_tree.SetItemData(hRoot, (DWORD_PTR)m_pControlJob);
    m_tree.SetItemState(hRoot, 0, TVIS_STATEIMAGEMASK);
    for (auto& item : m_pjWarps) {
        HTREEITEM hItem = m_tree.InsertItem(((SERVO::CProcessJob*)item.pj)->id().c_str(), 0, 0, hRoot);
        m_tree.SetItemData(hItem, (DWORD_PTR)&item);
        m_tree.SetItemState(hItem, INDEXTOSTATEIMAGEMASK(item.addToCj ? 2 : 1), TVIS_STATEIMAGEMASK);
    }
    m_tree.Expand(hRoot, TVE_EXPAND);
}
bool CControlJobManagerDlg::AddPorcessJob(SERVO::CProcessJob* pj)
{
    if (m_pControlJob == nullptr) return false;
    return m_pControlJob->addPjPointer(pj);
}
bool CControlJobManagerDlg::RemovePorcessJob(SERVO::CProcessJob* pj)
{
    if (m_pControlJob == nullptr) return false;
    return m_pControlJob->removePjPointer(pj->id());
}
void CControlJobManagerDlg::OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* p = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
    UINT oldState = p->itemOld.state, newState = p->itemNew.state;
    HTREEITEM hItem = p->itemNew.hItem;
    if (((oldState ^ newState) & TVIS_STATEIMAGEMASK) != 0) {
        const int idx = (newState & TVIS_STATEIMAGEMASK) >> 12; // 1=未选,2=已选
        const bool checked = (idx == 2);
        PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hItem);
        if (pjWarp != nullptr) {
            CString s; s.Format("%s %d", ((SERVO::CProcessJob*)pjWarp->pj)->id().c_str(),
                checked ? "" : "");
            AfxMessageBox(s);
        }
    }
    *pResult = 0;
}
// å‘½ä¸­å¤é€‰æ¡†ï¼šç”¨ NM_CLICK åšå‘½ä¸­æµ‹è¯•,然后“滞后”读取新状态
void CControlJobManagerDlg::OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult)
{
    *pResult = 0;
    DWORD pos = ::GetMessagePos();
    CPoint pt(GET_X_LPARAM(pos), GET_Y_LPARAM(pos));
    m_tree.ScreenToClient(&pt);
    TVHITTESTINFO ht{}; ht.pt = pt;
    HTREEITEM hItem = m_tree.HitTest(&ht);
    if (hItem && (ht.flags & TVHT_ONITEMSTATEICON)) {
        // è®© TreeView å…ˆåˆ‡æ¢ï¼Œå†å¼‚步读取最终状态
        PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
    }
}
// ç©ºæ ¼é”®ä¹Ÿä¼šåˆ‡æ¢å¤é€‰æ¡†
void CControlJobManagerDlg::OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult)
{
    *pResult = 0;
    auto* p = reinterpret_cast<LPNMTVKEYDOWN>(pNMHDR);
    if (p->wVKey == VK_SPACE) {
        HTREEITEM hItem = m_tree.GetSelectedItem();
        if (hItem) PostMessage(WM_AFTER_TVCHECK, (WPARAM)hItem, 0);
    }
}
// ç»Ÿä¸€å¤„理(读最终状态 + ä½ çš„业务)
LRESULT CControlJobManagerDlg::OnAfterTvCheck(WPARAM wParam, LPARAM /*lParam*/)
{
    HTREEITEM hItem = (HTREEITEM)wParam;
    if (!hItem) return 0;
    // åªå¤„理第二层:根的直接子节点(可选)
    auto getLevel = [&](HTREEITEM h) {
        int lv = 0; for (HTREEITEM p = m_tree.GetParentItem(h); p; p = m_tree.GetParentItem(p)) ++lv; return lv;
    };
    if (getLevel(hItem) != 1) return 0;
    BOOL checked = m_tree.GetCheck(hItem);
    // ä½ çš„业务逻辑(修正了 CString::Format çš„参数类型)
    auto* pjWarp = reinterpret_cast<PJWarp*>(m_tree.GetItemData(hItem));
    if (pjWarp) {
        pjWarp->addToCj = checked;
    }
    return 0;
}
void CControlJobManagerDlg::OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
    bool allow = FALSE;
    HTREEITEM hOldSel = pNMTreeView->itemOld.hItem;
    HTREEITEM hSel = pNMTreeView->itemNew.hItem;
    if (hSel != nullptr) {
        HTREEITEM hParent = m_tree.GetParentItem(hSel);
        if (hParent == nullptr) {
            SERVO::CControlJob* cj = (SERVO::CControlJob*)m_tree.GetItemData(hSel);
            ASSERT(m_pages.size() == 3);
            if (0 == ShowPage(2)) {
                SERVO::CControlJob* pControlJob = (SERVO::CControlJob*)m_tree.GetItemData(hSel);
                m_pages[2]->SetContext(pControlJob, 2);
            }
        }
        else if (m_tree.GetParentItem(hParent) == nullptr) {
            if (0 == ShowPage(1)) {
                PJWarp* pjWarp = (PJWarp*)m_tree.GetItemData(hSel);
                ((CCjPage2*)m_pages[1])->SetPjWarps(m_pjWarps);
                m_pages[1]->SetContext(pjWarp, 1);
            }
            else {
                allow = TRUE;
            }
        }
        else {
            // æœ‰ç¥–å…ˆ â†’ ç¬¬ä¸‰å±‚及以下 â†’ Glass
        }
    }
    *pResult = allow;
}
int CControlJobManagerDlg::ShowPage(int index)
{
    ASSERT(0 <= index && index <= 2);
    for (int i = 0; i < 3; i++) {
        if (m_pages[i]->IsWindowVisible()) {
            int ret = m_pages[i]->OnApply();
            if (ret != 0) return -1;
        }
        m_pages[i]->ShowWindow(index == i ? SW_SHOW : SW_HIDE);
    }
    return 0;
}
void CControlJobManagerDlg::OnDestroy()
{
    CDialogEx::OnDestroy();
    SaveState();
    for (auto page : m_pages) {
        page->DestroyWindow();
        delete page;
    }
}
void CControlJobManagerDlg::InitData()
{
    LoadState();
    if (m_pControlJob != nullptr) return;
    m_pControlJob = new SERVO::CControlJob("CJ" + CToolUnits::NowStrSec());
    char szBuffer[256];
    for (int i = 0; i < 4; i++) {
        sprintf_s(szBuffer, 256, "PJ%03d", i + 1);
        SERVO::CProcessJob* pj = new SERVO::CProcessJob(std::string(szBuffer));
        PJWarp pjWarp = {};
        pjWarp.pj = pj;
        pjWarp.port = -1;
        m_pjWarps.push_back(pjWarp);
    }
}
void CControlJobManagerDlg::OnBnClickedButtonApply()
{
    for (auto item : m_pages) {
        if (item->IsWindowVisible()) {
            if (0 == item->OnApply()) {
                GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
            }
        }
    }
}
void CControlJobManagerDlg::UpProcessJobId(PJWarp* pjWarp)
{
    // æ›´æ–°æ ‘控件
    // éåŽ†æ ¹èŠ‚ç‚¹
    HTREEITEM hRoot = m_tree.GetRootItem();
    while (hRoot) {
        // éåŽ†ç¬¬äºŒå±‚å­èŠ‚ç‚¹
        HTREEITEM hChild = m_tree.GetChildItem(hRoot);
        while (hChild) {
            DWORD_PTR data = m_tree.GetItemData(hChild);
            if ((void*)data == pjWarp) {
                SERVO::CProcessJob* pj = (SERVO::CProcessJob*)pjWarp->pj;
                m_tree.SetItemText(hChild, pj->id().c_str());
                return; // æ‰¾åˆ°å°±è¿”回
            }
            hChild = m_tree.GetNextSiblingItem(hChild);
        }
        hRoot = m_tree.GetNextSiblingItem(hRoot);
    }
}
void CControlJobManagerDlg::UpControlJobId(SERVO::CControlJob* pControlJob)
{
    // æ›´æ–°æ ‘控件
    // éåŽ†æ ¹èŠ‚ç‚¹
    HTREEITEM hRoot = m_tree.GetRootItem();
    if (hRoot != nullptr) {
        DWORD_PTR data = m_tree.GetItemData(hRoot);
        if ((void*)data == pControlJob) {
            m_tree.SetItemText(hRoot, pControlJob->id().c_str());
            return; // æ‰¾åˆ°å°±è¿”回
        }
    }
}
void CControlJobManagerDlg::LoadState()
{
    if (!m_bHasState) return;
    // æŠŠ s_state -> æˆå‘˜å˜é‡
    m_pControlJob = m_state.pControlJob;
    m_pjWarps = m_state.pjWarps;
}
void CControlJobManagerDlg::SaveState()
{
    m_state.pControlJob = m_pControlJob;
    m_state.pjWarps = m_pjWarps;
    m_bHasState = true;
}
void CControlJobManagerDlg::OnBnClickedButtonBathCompletion()
{
    // å…ˆæ£€æŸ¥å½“前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()) {
            int ret = m_pages[i]->OnApply();
            if (ret != 0) return ;
        }
    }
    GetDlgItem(IDC_BUTTON_APPLY)->EnableWindow(FALSE);
    // å…ˆæ£€æŸ¥æ•°æ®æ­£ç¡®æ€§
    int checkCount = 0;
    for (auto item : m_pjWarps) {
        if (!item.addToCj) continue;
        checkCount++;
    }
    if (checkCount == 0) {
        AfxMessageBox(_T("您没有选择要进行工艺处理的Process Job!\n请在要进行工艺处理的Process Job前打勾。"));
        return;
    }
    SERVO::CLoadPort* pPorts[4];
    pPorts[0] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT1);
    pPorts[1] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT2);
    pPorts[2] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT3);
    pPorts[3] = (SERVO::CLoadPort*)master.getEquipment(EQ_ID_LOADPORT4);
    bool bProcessStart[] = {false, false, false, false};
    std::vector<SERVO::CProcessJob*> pjs;
    for (auto item : m_pjWarps) {
        if (!item.addToCj) continue;
        if (item.port == -1) continue;
        BOOL bCheck = FALSE;
        for (int i = 0; i < 8; i++) {
            if (item.checkSlot[i]) {
                bCheck = TRUE;
                break;
            }
        }
        if (!bCheck) continue;
        SERVO::CProcessJob* pScr = (SERVO::CProcessJob*)item.pj;
        pScr->setPjWarp(item);
        pScr->setLotId("LotID1");
        pScr->setProductId("ProductId1");
        pScr->setOperationId("OperationId");
        pScr->setRecipe(SERVO::RecipeMethod::NoTuning, pScr->recipeSpec());
        SERVO::CProcessJob * pj = new SERVO::CProcessJob(pScr->id());
        pj->setPjWarp(item);
        pj->setLotId("LotID1");
        pj->setProductId("ProductId1");
        pj->setOperationId("OperationId");
        pj->setRecipe(SERVO::RecipeMethod::NoTuning, pScr->recipeSpec());
        std::vector<SERVO::CarrierSlotInfo> carriers;
        SERVO::CarrierSlotInfo csi;
        csi.carrierId = pPorts[item.port]->getCassetteId();
        for (int i = 0; i < 8; i++) {
            if (item.checkSlot[i]) {
                SERVO::CGlass* pGlass = pPorts[item.port]->getGlassFromSlot(i+1);
                if (pGlass != nullptr) {
                    csi.slots.push_back(i + 1);
                }
            }
        }
        carriers.push_back(csi);
        pj->setCarriers(carriers);
        pjs.push_back(pj);
        bProcessStart[item.port] = true;
        m_pControlJob->addPJ(pScr->id());
    }
    if (pjs.empty()) {
        AfxMessageBox(_T("没有需要进行工艺处理的Process Job!\n可能未选择Port或选择任何物料。"));
        return;
    }
    m_pControlJob->setPJs(pjs);
    m_pControlJob->clearIssues();
    int nRet = master.setProcessJobs(pjs);
    // æ²¡æœ‰é—®é¢˜çš„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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
#pragma once
#include "CCjPage1.h"
#include "CCjPage2.h"
#include "CCjPage3.h"
#include "ApredTreeCtrl2.h"
#define WM_AFTER_TVCHECK (WM_USER + 1000)
// CControlJobManagerDlg å¯¹è¯æ¡†
class CControlJobManagerDlg : public CDialogEx
{
    DECLARE_DYNAMIC(CControlJobManagerDlg)
public:
    CControlJobManagerDlg(CWnd* pParent = nullptr);   // æ ‡å‡†æž„造函数
    virtual ~CControlJobManagerDlg();
    static void FreeState();
private:
    void Resize();
    void UpdateCtrlState();
    void UpdateControlJob();
    bool AddPorcessJob(SERVO::CProcessJob* pj);
    bool RemovePorcessJob(SERVO::CProcessJob* pj);
    void UpProcessJobId(PJWarp* pjWarp);
    void UpControlJobId(SERVO::CControlJob* pControlJob);
    void InitData();
    void LoadState();
    void SaveState();
    int ShowPage(int index);
private:
    std::vector<CCjPageBase*> m_pages;
    SERVO::CControlJob* m_pControlJob;
    std::vector<PJWarp> m_pjWarps;
    CApredTreeCtrl2 m_tree;
public:
    struct State {
        SERVO::CControlJob* pControlJob = nullptr;
        std::vector<PJWarp> pjWarps;
    };
    static bool m_bHasState;
    static State m_state;
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_DIALOG_CONTROL_JOB_MANAGER };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI);
    afx_msg void OnTvnItemChangedTree(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnDestroy();
    afx_msg void OnBnClickedButtonApply();
    afx_msg void OnTvnSelchangingTree1(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnBnClickedButtonBathCompletion();
    afx_msg void OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg void OnTreeKeyDown(NMHDR* pNMHDR, LRESULT* pResult);
    afx_msg LRESULT OnAfterTvCheck(WPARAM wParam, LPARAM lParam);
};
SourceCode/Bond/Servo/CEquipment.cpp
@@ -26,7 +26,6 @@
        m_pCclink = nullptr;
        m_nBaseAlarmId = 0;
        m_pArm = nullptr;
        m_processState = PROCESS_STATE::Ready;
        m_blockReadBit = { 0 };
        m_nTestFlag = 0;
        InitializeCriticalSection(&m_criticalSection);
@@ -144,13 +143,15 @@
        return 0;
    }
    void CEquipment::setProcessState(PROCESS_STATE state)
    void CEquipment::setProcessState(int nSlotNo, PROCESS_STATE state)
    {
        m_processState = state;
        onProcessStateChanged(m_processState);
        if (nSlotNo <= 0 || nSlotNo > 8) return;
        m_processState[nSlotNo - 1] = state;
        onProcessStateChanged(nSlotNo, m_processState[nSlotNo - 1]);
        if (m_listener.onProcessStateChanged != nullptr) {
            m_listener.onProcessStateChanged(this, m_processState);
            m_listener.onProcessStateChanged(this, nSlotNo, m_processState[nSlotNo - 1]);
        }
    }
@@ -909,8 +910,8 @@
        Unlock();
        if (m_processState != PROCESS_STATE::Ready) {
            setProcessState(PROCESS_STATE::Ready);
        if (m_processState[port] != PROCESS_STATE::Ready) {
            setProcessState(port, PROCESS_STATE::Ready);
        }
        if (m_listener.onDataChanged != nullptr) {
@@ -941,9 +942,11 @@
        pGlass->release();                // tempFetchOut需要调用一次release
        Unlock();
        /*
        if (m_processState != PROCESS_STATE::Processing) {
            setProcessState(PROCESS_STATE::Processing);
        }
        */
        if (m_listener.onDataChanged != nullptr) {
            m_listener.onDataChanged(this, EDCC_STORED_JOB);
@@ -1595,7 +1598,7 @@
        auto rawData = processData.getParamsRawData();
        std::vector<CParam> tempParams;
        this->parsingParams((const char*)rawData.data(), rawData.size(), tempParams);
        this->parsingProcessData((const char*)rawData.data(), rawData.size(), tempParams);
        int n = processData.getTotalParameter();
        std::vector<CParam> params(tempParams.begin(), tempParams.begin() + min(n, (int)tempParams.size()));
        pGlass->addParams(params);
@@ -1929,12 +1932,17 @@
            year, month, day, hour, minute, second
            );
        CGlass* pGlass = getGlassFromSlot(slotNo);
        if (pGlass == nullptr) {
            LOGE("<CEquipment-%s>decodeJobProcessStartReport, æ‰¾ä¸åˆ°å¯¹åº”glass", getName().c_str());
        }
        if (slotNo <= 0 || slotNo > 8) return -1;
        if (m_processState != PROCESS_STATE::Processing) {
        if (m_processState[slotNo -1] != PROCESS_STATE::Processing) {
            Lock();
            m_svDatas.clear();
            Unlock();
            setProcessState(PROCESS_STATE::Processing);
            setProcessState(slotNo, PROCESS_STATE::Processing);
        }
@@ -2002,11 +2010,11 @@
        );
        if (m_processState != PROCESS_STATE::Complete) {
            setProcessState(PROCESS_STATE::Complete);
        }
        CGlass* pGlass = getGlassFromSlot(slotNo);
        if (m_processState[slotNo - 1] != PROCESS_STATE::Complete) {
            setProcessState(slotNo, PROCESS_STATE::Complete);
        }
        if (pGlass == nullptr) {
            LOGE("<CEquipment-%s>decodeJobProcessEndReport, æ‰¾ä¸åˆ°å¯¹åº”glass", getName().c_str());
        }
@@ -2015,9 +2023,6 @@
            if (pJs->getCassetteSequenceNo() == cassetteNo
                && pJs->getJobSequenceNo() == jobSequenceNo) {
                pGlass->processEnd(m_nID, getSlotUnit(slotNo));
                if (m_processState != PROCESS_STATE::Complete) {
                    setProcessState(PROCESS_STATE::Complete);
                }
            }
            else {
                LOGE("<CEquipment-%s>decodeJobProcessEndReport, jobSequenceNo或jobSequenceNo不匹配",
@@ -2136,7 +2141,7 @@
        return 0;
    }
    int CEquipment::onProcessStateChanged(PROCESS_STATE state)
    int CEquipment::onProcessStateChanged(int nSlotNo, PROCESS_STATE state)
    {
        return 0;
    }
@@ -2237,7 +2242,7 @@
                return -1;
            });
        pStep->setName(STEP_EQ_FAC_DATA_REPORT);
        pStep->setProp("Port", (void*)port);
        pStep->setProp("Port", (void*)(__int64)port);
        pStep->setWriteSignalDev(writeSignalDev);
        if (addStep(STEP_ID_FAC_DATA_REPORT, pStep) != 0) {
            delete pStep;
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";
@@ -430,4 +431,147 @@
        return strOut;
    }
    // ========== SV数据管理接口实现 ==========
    void CGlass::addSVData(int machineId, const std::string& dataType, const SVDataItem& dataItem) {
        m_svDatas[machineId][dataType].push_back(dataItem);
    }
    void CGlass::addSVData(int machineId, const std::string& dataType, double value) {
        auto now = std::chrono::system_clock::now();
        m_svDatas[machineId][dataType].emplace_back(now, value);
    }
    void CGlass::addSVData(int machineId, const std::string& dataType, int64_t timestamp, double value) {
        // å°†int64_t时间戳转换为system_clock::time_point
        std::chrono::system_clock::time_point timePoint{
            std::chrono::milliseconds(timestamp)  // å‡è®¾timestamp是毫秒
            // å¦‚果是秒,使用:std::chrono::seconds(timestamp)
        };
        m_svDatas[machineId][dataType].emplace_back(timePoint, value);
    }
    void CGlass::addSVData(int machineId, const std::string& dataType, const std::vector<SVDataItem>& dataItems) {
        auto& dataList = m_svDatas[machineId][dataType];
        dataList.insert(dataList.end(), dataItems.begin(), dataItems.end());
    }
    std::vector<SVDataItem> CGlass::getSVData(int machineId, const std::string& dataType) const {
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            auto dataIt = machineIt->second.find(dataType);
            if (dataIt != machineIt->second.end()) {
                return dataIt->second;
            }
        }
        return std::vector<SVDataItem>();
    }
    std::vector<std::string> CGlass::getSVDataTypes(int machineId) const {
        std::vector<std::string> types;
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            for (const auto& pair : machineIt->second) {
                types.push_back(pair.first);
            }
        }
        return types;
    }
    std::unordered_map<std::string, std::vector<SVDataItem>> CGlass::getMachineSVData(int machineId) const {
        auto it = m_svDatas.find(machineId);
        if (it != m_svDatas.end()) {
            return it->second;
        }
        return std::unordered_map<std::string, std::vector<SVDataItem>>();
    }
    const std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>>& CGlass::getAllSVData() const {
        return m_svDatas;
    }
    bool CGlass::hasSVData(int machineId, const std::string& dataType) const {
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            return machineIt->second.find(dataType) != machineIt->second.end();
        }
        return false;
    }
    bool CGlass::hasMachineSVData(int machineId) const {
        return m_svDatas.find(machineId) != m_svDatas.end();
    }
    std::vector<int> CGlass::getMachineIdsWithSVData() const {
        std::vector<int> machineIds;
        for (const auto& pair : m_svDatas) {
            machineIds.push_back(pair.first);
        }
        return machineIds;
    }
    void CGlass::clearSVData(int machineId, const std::string& dataType) {
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            machineIt->second.erase(dataType);
            // å¦‚果该机器没有其他数据了,也清除机器条目
            if (machineIt->second.empty()) {
                m_svDatas.erase(machineIt);
            }
        }
    }
    void CGlass::clearMachineSVData(int machineId) {
        m_svDatas.erase(machineId);
    }
    void CGlass::clearAllSVData() {
        m_svDatas.clear();
    }
    size_t CGlass::getSVDataCount(int machineId, const std::string& dataType) const {
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            auto dataIt = machineIt->second.find(dataType);
            if (dataIt != machineIt->second.end()) {
                return dataIt->second.size();
            }
        }
        return 0;
    }
    size_t CGlass::getMachineSVDataCount(int machineId) const {
        size_t total = 0;
        auto machineIt = m_svDatas.find(machineId);
        if (machineIt != m_svDatas.end()) {
            for (const auto& pair : machineIt->second) {
                total += pair.second.size();
            }
        }
        return total;
    }
    size_t CGlass::getTotalSVDataCount() const {
        size_t total = 0;
        for (const auto& machinePair : m_svDatas) {
            for (const auto& dataPair : machinePair.second) {
                total += dataPair.second.size();
            }
        }
        return total;
    }
    std::vector<std::pair<int, SVDataItem>> CGlass::findSVDataByType(const std::string& dataType) const {
        std::vector<std::pair<int, SVDataItem>> result;
        for (const auto& machinePair : m_svDatas) {
            auto dataIt = machinePair.second.find(dataType);
            if (dataIt != machinePair.second.end()) {
                for (const auto& item : dataIt->second) {
                    result.emplace_back(machinePair.first, item);
                }
            }
        }
        return result;
    }
}
SourceCode/Bond/Servo/CGlass.h
@@ -12,6 +12,15 @@
namespace SERVO {
    /// æ•°æ®é¡¹ï¼šæ—¶é—´æˆ³ + æ•°å€¼
    struct SVDataItem {
        std::chrono::system_clock::time_point timestamp;
        double value;  // æˆ–者根据实际情况使用其他类型
        SVDataItem(std::chrono::system_clock::time_point ts, double val)
            : timestamp(ts), value(val) {}
    };
    /// PJ ç”Ÿå‘½å‘¨æœŸï¼ˆè´´è¿‘ E40 å¸¸è§çŠ¶æ€ï¼‰
    enum class GlsState : uint8_t {
        NoState = 0,
@@ -100,6 +109,58 @@
        void addParams(std::vector<CParam>& params);
        std::vector<CParam>& getParams();
        // ========== SV数据管理接口(新设计)==========
        // æ·»åŠ æ•°æ®åˆ°æŒ‡å®šæœºå™¨çš„æŒ‡å®šæ•°æ®ç±»åž‹
        void addSVData(int machineId, const std::string& dataType, const SVDataItem& dataItem);
        void addSVData(int machineId, const std::string& dataType, double value); // è‡ªåŠ¨ä½¿ç”¨å½“å‰æ—¶é—´
        void addSVData(int machineId, const std::string& dataType, int64_t timestamp, double value);
        // æ‰¹é‡æ·»åŠ æ•°æ®åˆ°æŒ‡å®šæœºå™¨çš„æŒ‡å®šæ•°æ®ç±»åž‹
        void addSVData(int machineId, const std::string& dataType, const std::vector<SVDataItem>& dataItems);
        // èŽ·å–æŒ‡å®šæœºå™¨çš„æŒ‡å®šæ•°æ®ç±»åž‹çš„æ‰€æœ‰æ•°æ®
        std::vector<SVDataItem> getSVData(int machineId, const std::string& dataType) const;
        // èŽ·å–æŒ‡å®šæœºå™¨çš„æ‰€æœ‰æ•°æ®ç±»åž‹
        std::vector<std::string> getSVDataTypes(int machineId) const;
        // èŽ·å–æŒ‡å®šæœºå™¨çš„æ‰€æœ‰æ•°æ®ï¼ˆæŒ‰æ•°æ®ç±»åž‹ç»„ç»‡ï¼‰
        std::unordered_map<std::string, std::vector<SVDataItem>> getMachineSVData(int machineId) const;
        // èŽ·å–æ‰€æœ‰æœºå™¨çš„æ•°æ®
        const std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>>& getAllSVData() const;
        // æ£€æŸ¥æŒ‡å®šæœºå™¨æ˜¯å¦æœ‰æŒ‡å®šç±»åž‹çš„æ•°æ®
        bool hasSVData(int machineId, const std::string& dataType) const;
        // æ£€æŸ¥æŒ‡å®šæœºå™¨æ˜¯å¦æœ‰ä»»ä½•数据
        bool hasMachineSVData(int machineId) const;
        // èŽ·å–æ‰€æœ‰æœ‰SV数据的机器ID
        std::vector<int> getMachineIdsWithSVData() const;
        // æ¸…空指定机器的指定数据类型的数据
        void clearSVData(int machineId, const std::string& dataType);
        // æ¸…空指定机器的所有数据
        void clearMachineSVData(int machineId);
        // æ¸…空所有机器的所有数据
        void clearAllSVData();
        // èŽ·å–æŒ‡å®šæœºå™¨çš„æŒ‡å®šæ•°æ®ç±»åž‹çš„æ•°æ®æ•°é‡
        size_t getSVDataCount(int machineId, const std::string& dataType) const;
        // èŽ·å–æŒ‡å®šæœºå™¨çš„æ€»æ•°æ®æ•°é‡
        size_t getMachineSVDataCount(int machineId) const;
        // èŽ·å–æ‰€æœ‰æœºå™¨çš„æ€»æ•°æ®æ•°é‡
        size_t getTotalSVDataCount() const;
        // èŽ·å–æŒ‡å®šæ•°æ®ç±»åž‹åœ¨æ‰€æœ‰æœºå™¨ä¸­çš„æ•°æ®
        std::vector<std::pair<int, SVDataItem>> findSVDataByType(const std::string& dataType) const;
    private:
        MaterialsType m_type;
        std::string m_strID;
@@ -112,6 +173,9 @@
        BOOL m_bScheduledForProcessing;            /* æ˜¯å¦å°†åŠ å·¥å¤„ç† */
        CProcessJob* m_pProcessJob;
        std::vector<CParam> m_params;            // å·¥è‰ºå‚æ•°
        // æ–°çš„三层数据结构:机器ID -> æ•°æ®ç±»åž‹ -> æ•°æ®åˆ—表
        std::unordered_map<int, std::unordered_map<std::string, std::vector<SVDataItem>>> m_svDatas;
    };
}
SourceCode/Bond/Servo/CJobDataS.cpp
@@ -618,9 +618,9 @@
        index += sizeof(short);
        memcpy(&m_nQTime[1], &pszBuffer[index], sizeof(short));
        index += sizeof(int);
        index += sizeof(short);
        memcpy(&m_nQTime[2], &pszBuffer[index], sizeof(int));
        memcpy(&m_nQTime[2], &pszBuffer[index], sizeof(short));
        index += sizeof(short);
        memcpy(&m_nQTimeOverFlag, &pszBuffer[index], sizeof(short));
SourceCode/Bond/Servo/CLoadPort.cpp
@@ -353,7 +353,7 @@
        // ä»Žé…ç½®è¯»å‡ºçš„enable,初始化时写给efem
        static int i_enable[4] = { 0 };
        if ((++i_enable[m_nIndex]) == 10 + m_nIndex) {
        if ((++i_enable[m_nIndex]) == 20 + m_nIndex) {
            eablePort(m_bEnable, [&](int code) -> int {
                LOGI("<LoadPort-%d>eablePort:code=%d", m_nIndex, code);
                return 0;
@@ -404,7 +404,6 @@
            m_portStatusReport.serialize(ar);
        }
        else {
            int temp;
            ar >> m_nIndex;
            m_portStatusReport.serialize(ar);
        }
@@ -506,6 +505,9 @@
    void CLoadPort::setIndex(unsigned int index)
    {
        m_nIndex = index;
        std::string id = "Port" + std::to_string(index + 1);
        m_portStatusReport.setCassetteId(id.c_str());
    }
    unsigned int CLoadPort::getIndex()
SourceCode/Bond/Servo/CMaster.cpp
@@ -6,9 +6,15 @@
#include "RecipeManager.h"
#include <fstream>
#include "SerializeUtil.h"
#include "CServoUtilsTool.h"
namespace SERVO {
    static inline int64_t now_ms_epoch() {
        using namespace std::chrono;
        return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
    }
    CMaster* g_pMaster = NULL;
    unsigned __stdcall DispatchThreadFunction(LPVOID lpParam)
@@ -62,6 +68,8 @@
        m_bBatch = false;
        m_nContinuousTransferCount = 0;
        m_nContinuousTransferStep = CTStep_Unknow;
        m_nContinuousWorkingPort = 0;
        m_nContinuousWorkingSlot = 0;
        m_pControlJob = nullptr;
        m_nTestFlag = 0;
        InitializeCriticalSection(&m_criticalSection);
@@ -211,10 +219,9 @@
        // è¯»ç¼“存数据
        readCache();
        loadState();
        // å®šæ—¶å™¨
@@ -230,6 +237,10 @@
        // ç›‘控bit线程
        m_hReadBitsThreadHandle = (HANDLE)_beginthreadex(NULL, 0, SERVO::ReadBitsThreadFunction, this,
            0, &m_nReadBitsThreadAddr);
        // æ›²çº¿æœåŠ¡
        CreateDAQBridgeServer();
        LOGI("<Master>初始化完成.");
@@ -261,6 +272,12 @@
        }
        m_listEquipment.clear();
        if (m_pCollector != nullptr) {
            m_pCollector->stopLoop();
            delete m_pCollector;
            m_pCollector = nullptr;
        }
        return 0;
    }
@@ -804,48 +821,32 @@
            // æ‰¹å¤„理模式,最终以此为准,但先保留之前的单片模式
            else if (m_state == MASTERSTATE::RUNNING_BATCH) {
                // é¦–选检查有没有CControlJob, çŠ¶æ€ç­‰
                if (m_pControlJob == nullptr) {
                // 1) æŽ§åˆ¶ä½œä¸šç”Ÿå‘½å‘¨æœŸä¿éšœ
                if (m_pControlJob == nullptr) { unlock(); continue; }
                CJState cjst = m_pControlJob->state();
                if (cjst == CJState::Completed || cjst == CJState::Aborted || cjst == CJState::Failed) {
                    unlock();
                    continue;
                }
                CJState state = m_pControlJob->state();
                if (state == CJState::Completed || state == CJState::Aborted || state == CJState::Failed) {
                    // ConrolJpb已完成
                    LOGE("<Master>ControlJob已经完成或失败中断");
                    unlock();
                    continue;
                }
                if (m_pControlJob->state() == CJState::NoState) {
                if (cjst == CJState::NoState) {
                    LOGI("<Master>ControlJob已经进入列队");
                    m_pControlJob->queue();
                }
                if (m_pControlJob->state() == CJState::Queued) {
                    LOGI("<Master>ControlJob已经启动");
                    m_pControlJob->start();
                    if (m_listener.onCjStart != nullptr) {
                        m_listener.onCjStart(this, m_pControlJob);
                    }
                    if (m_listener.onCjStart) m_listener.onCjStart(this, m_pControlJob);
                }
                if (m_pControlJob->state() == CJState::Paused) {
                    LOGI("<Master>ControlJob已经恢复运行");
                    m_pControlJob->resume();
                }
                // å¦‚果当前未选择CProcessJob, é€‰æ‹©ä¸€ä¸ª
                // 2) è‹¥å½“前无 PJ,则选择一个并上报
                if (m_inProcesJobs.empty()) {
                    auto pj = acquireNextProcessJob();
                    if (pj != nullptr) {
                    if (auto pj = acquireNextProcessJob()) {
                        m_inProcesJobs.push_back(pj);
                        // è¿™é‡Œä¸ŠæŠ¥PJ Start事件
                        if (m_listener.onPjStart != nullptr) {
                            m_listener.onPjStart(this, pj);
                        }
                        if (m_listener.onPjStart) m_listener.onPjStart(this, pj);
                    }
                }
                if (m_inProcesJobs.empty()) {
@@ -854,54 +855,64 @@
                    continue;
                }
                // å¦‚果当前没有Glass, é€‰æ‹©
                // 3) è‹¥é˜Ÿåˆ—æ—  Glass,拉取到等待队列
                if (m_queueGlasses.empty()) {
                    int nCount = acquireGlassToQueue();
                    LOGI("<Master>已加入 %d å—Glass到工艺列队!", nCount);
                    if (nCount > 0) {
                        LOGI("<Master>已加入 %d å—Glass到工艺列队!", nCount);
                    }
                }
                // æ£€æµ‹åˆ¤æ–­robot状态
                // 4) æœºå™¨äººçŠ¶æ€
                RMDATA& rmd = pEFEM->getRobotMonitoringData();
                if (rmd.status != ROBOT_STATUS::Idle && rmd.status != ROBOT_STATUS::Run) {
                    unlock();
                    continue;
                    unlock(); continue;
                }
                // 5) æ­£åœ¨æ‰§è¡Œçš„ RobotTask å…ˆè®©å®ƒè·‘完一拍
                if (m_pActiveRobotTask != nullptr) {
                    if (m_pActiveRobotTask->isPicked()) {
                        m_pActiveRobotTask->place();
                    }
                    unlock();
                    // æ£€æµ‹åˆ°å½“前有正在下午的任务,确保当前任务完成或中止后继续
                    // LOGI("检测到当前有正在下午的任务,确保当前任务完成或中止后继续...");
                    unlock(); // ç­‰å½“前任务完成或中止后继续
                    continue;
                }
                // æ­¤å¤„检测优先类型和次要类型(G1或G2)
                // å¦‚果其中一Bonder有单个玻璃,优先取它的配对类型,否则无所谓了
                primaryType = MaterialsType::G1;
                secondaryType = MaterialsType::G2;
                if ((!pBonder1->canPlaceGlassInSlot(0) && !pBonder1->canPlaceGlassInSlot(1))
                    && (!pBonder2->canPlaceGlassInSlot(0) && !pBonder2->canPlaceGlassInSlot(1))) {
                    // å¦‚æžœG1和G2都满了,那就看Aligner, å¦‚æžœAligner有玻璃为G1, åˆ™å–G2
                    CGlass* pGlass = pAligner->getGlassFromSlot(1);
                    if (pGlass != nullptr && pGlass->getType() == MaterialsType::G1) {
                        primaryType = MaterialsType::G2;
                        secondaryType = MaterialsType::G1;
                // 6) â€”—关键:全局统计 G1/G2 ä¸Žç»„数门限(与单片分支对齐)——
                auto countG1G2 = [&]() {
                    int g1 = 0, g2 = 0;
                    if (pBonder1->slotHasGlass(0)) g2++;
                    if (pBonder1->slotHasGlass(1)) g1++;
                    if (pBonder2->slotHasGlass(0)) g2++;
                    if (pBonder2->slotHasGlass(1)) g1++;
                    if (pFliper->slotHasGlass(0))  g2++;
                    if (pVacuumBake->slotHasGlass(0)) g1++;
                    if (pVacuumBake->slotHasGlass(1)) g1++;
                    if (auto g = pAligner->getGlassFromSlot(0)) {
                        auto t = g->getType();
                        if (t == MaterialsType::G1) g1++; else if (t == MaterialsType::G2) g2++;
                    }
                }
                else if ((pBonder1->canPlaceGlassInSlot(0) && !pBonder1->canPlaceGlassInSlot(1))
                    || (pBonder2->canPlaceGlassInSlot(0) && !pBonder2->canPlaceGlassInSlot(1))) {
                    primaryType = MaterialsType::G2;
                    secondaryType = MaterialsType::G1;
                }
                    return std::pair<int, int>(g1, g2);
                };
                int g1Count = 0, g2Count = 0;
                std::tie(g1Count, g2Count) = countG1G2();
                int nGlassGroup = min(g1Count, g2Count);
                int nExtraType = (g1Count == g2Count ? 0 : (g1Count > g2Count ? 1 : 2));
                // Measurement -> LoadPort
                // primary/secondary ç»Ÿä¸€å®šä¹‰ï¼ˆsecondary é»˜è®¤ G0)
                MaterialsType primaryType = MaterialsType::G1;
                MaterialsType secondaryType = MaterialsType::G0;
                if (nExtraType == 0) primaryType = MaterialsType::G2; // ä¸Žå•片分支一致
                else                 primaryType = MaterialsType::G1;
                // ç»„数门限:≥2 ç»„时不再从 LP ä¸Šç‰‡ï¼Œé¿å…å †ç§¯ï¼ˆä¸Žå•片一致)
                bool blockLoadFromLP = (nGlassGroup >= 2);
                // 7) Measurement -> LoadPort(固定:G1 ä¼˜å…ˆå›ž LP)
                if (rmd.armState[0] || rmd.armState[1]) {
                    LOGD("Arm1 %s, Arm2 %s.", rmd.armState[0] ? _T("不可用") : _T("可用"),
                    LOGD("Arm1 %s, Arm2 %s.",
                        rmd.armState[0] ? _T("不可用") : _T("可用"),
                        rmd.armState[1] ? _T("不可用") : _T("可用"));
                }
                for (int s = 0; s < 4; s++) {
@@ -909,140 +920,123 @@
                    if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
                        && (pt == PortType::Unloading || pt == PortType::Both)
                        && pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
                        m_pActiveRobotTask = createTransferTask(pMeasurement, pLoadPorts[s], primaryType, secondaryType);
                        if (m_pActiveRobotTask != nullptr) {
                            goto BATCH_PORT_PUT;
                        }
                        m_pActiveRobotTask = createTransferTask(pMeasurement, pLoadPorts[s], MaterialsType::G1, secondaryType);
                        if (m_pActiveRobotTask != nullptr) { goto BATCH_PORT_PUT; }
                    }
                }
            BATCH_PORT_PUT:
                BATCH_PORT_PUT:
                CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                // Measurement NG -> LoadPort
                // NG回原位
                // 8) Measurement NG -> LoadPort(原位回退)
                if (!rmd.armState[1]) {
                    m_pActiveRobotTask = createTransferTask_restore(pMeasurement, pLoadPorts);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // BakeCooling ->Measurement
                // 9) BakeCooling -> Measurement
                if (!rmd.armState[0]) {
                    m_pActiveRobotTask = createTransferTask_bakecooling_to_measurement(pBakeCooling, pMeasurement);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // BakeCooling内部
                // Bake -> Cooling
                // 10) BakeCooling å†…部(Bake -> Cooling)
                if (!rmd.armState[0]) {
                    m_pActiveRobotTask = createTransferTask_bake_to_cooling(pBakeCooling);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // Bonder -> BakeCooling
                // 11) Bonder -> BakeCooling
                if (!rmd.armState[0]) {
                    m_pActiveRobotTask = createTransferTask_bonder_to_bakecooling(pBonder1, pBakeCooling);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                if (!rmd.armState[0]) {
                    m_pActiveRobotTask = createTransferTask_bonder_to_bakecooling(pBonder2, pBakeCooling);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // 12) Fliper(G2) -> Bonder(前置:VacuumBake æœ‰ processed G1;输出 G2 åˆ° Bonder slot0)
                if (auto pSrcSlot = pVacuumBake->getProcessedSlot(MaterialsType::G1)) {
                    if (!rmd.armState[1] && pBonder1->canPlaceGlassInSlot(0)) {
                        m_pActiveRobotTask = createTransferTask(pFliper, pBonder1, MaterialsType::G2, MaterialsType::G0, 2);
                        CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                    }
                    if (!rmd.armState[1] && pBonder2->canPlaceGlassInSlot(0)) {
                        m_pActiveRobotTask = createTransferTask(pFliper, pBonder2, MaterialsType::G2, MaterialsType::G0, 2);
                        CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                    }
                }
                // Fliper(G2) -> Bonder
                auto pSrcSlot = pVacuumBake->getProcessedSlot(primaryType);
                if (pSrcSlot != nullptr && !rmd.armState[1] && !pBonder1->hasBondGlass()) {
                    m_pActiveRobotTask = createTransferTask(pFliper, pBonder1, primaryType, secondaryType, 2);
                // 13) VacuumBake(G1) -> Bonder(槽级判定:slot0(G2) å·²æœ‰ä¸” slot1(G1) ä¸ºç©ºï¼‰
                if (!rmd.armState[0] && pBonder1->slotHasGlass(0) && !pBonder1->slotHasGlass(1)) {
                    m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder1, MaterialsType::G1, MaterialsType::G0);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                if (pSrcSlot != nullptr && !rmd.armState[1] && !pBonder2->hasBondGlass()) {
                    m_pActiveRobotTask = createTransferTask(pFliper, pBonder2, primaryType, secondaryType, 2);
                if (!rmd.armState[0] && pBonder2->slotHasGlass(0) && !pBonder2->slotHasGlass(1)) {
                    m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder2, MaterialsType::G1, MaterialsType::G0);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // VacuumBake(G1) -> Bonder
                if (!rmd.armState[0] && !pBonder1->hasBondGlass()) {
                    m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder1, primaryType, secondaryType);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                if (!rmd.armState[0] && !pBonder2->hasBondGlass()) {
                    m_pActiveRobotTask = createTransferTask(pVacuumBake, pBonder2, primaryType, secondaryType);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // Aligner -> Fliper(G2)
                // Aligner -> VacuumBake(G1)
                // 14) Aligner -> Fliper(G2) ä»¥åŠ -> VacuumBake(G1)(固定映射)
                if (!rmd.armState[1]) {
                    m_pActiveRobotTask = createTransferTask(pAligner, pFliper, primaryType, secondaryType);
                    m_pActiveRobotTask = createTransferTask(pAligner, pFliper, MaterialsType::G2, MaterialsType::G0);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                if (!rmd.armState[0]) {
                    m_pActiveRobotTask = createTransferTask(pAligner, pVacuumBake, primaryType, secondaryType);
                    m_pActiveRobotTask = createTransferTask(pAligner, pVacuumBake, MaterialsType::G1, MaterialsType::G0);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // Aligner -> LoadPort
                // 15) Aligner -> LoadPort(restore)
                if (!rmd.armState[1]) {
                    m_pActiveRobotTask = createTransferTask_restore(pAligner, pLoadPorts);
                    CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                }
                // 16) LoadPort -> Aligner(受组数门限控制;统一 buddy/状态时序)
                if (blockLoadFromLP) { unlock(); continue; }
                // LoadPort -> Aligner
                for (int s = 0; s < 4; s++) {
                    PortType pt = pLoadPorts[s]->getPortType();
                    if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
                        && (pt == PortType::Loading || pt == PortType::Both)
                        && pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
                        m_pActiveRobotTask = createTransferTask(pLoadPorts[s], pAligner, primaryType, secondaryType, m_bJobMode);
                        m_pActiveRobotTask = createTransferTask(pLoadPorts[s], pAligner, primaryType, secondaryType, 1, m_bJobMode);
                        if (m_pActiveRobotTask != nullptr) {
                            CGlass* pGlass = (CGlass*)m_pActiveRobotTask->getContext();
                            auto* pGlass = static_cast<CGlass*>(m_pActiveRobotTask->getContext());
                            if (pGlass->getBuddy() != nullptr) {
                                delete m_pActiveRobotTask;
                                m_pActiveRobotTask = nullptr;
                                delete m_pActiveRobotTask; m_pActiveRobotTask = nullptr;
                                continue;
                            }
                            pEFEM->setContext(pGlass);
                            // ç»Ÿä¸€ï¼šqueue -> start -> setContext -> move queue→inProcess -> onPanelStart
                            pGlass->queue();
                            pGlass->start();
                            pEFEM->setContext(pGlass);
                            bool bMoved = glassFromQueueToInPorcess(pGlass);
                            if (bMoved) {
                                LOGI("<Master>Glass(%s)从等待列队到工艺列队转移成功.",
                                    pGlass->getID().c_str());
                                LOGI("<Master>Glass(%s)从等待列队到工艺列队转移成功.", pGlass->getID().c_str());
                            }
                            else {
                                LOGE("<Master>Glass(%s)从等待列队到工艺列队转移失败.",
                                    pGlass->getID().c_str());
                                LOGE("<Master>Glass(%s)从等待列队到工艺列队转移失败.", pGlass->getID().c_str());
                            }
                            // è¿™é‡Œä¸ŠæŠ¥Panel Start事件
                            if (m_listener.onPanelStart != nullptr) {
                                m_listener.onPanelStart(this, pGlass);
                            }
                            if (m_listener.onPanelStart) m_listener.onPanelStart(this, pGlass);
                            goto BATCH_PORT_GET;
                        }
                    }
                }
            BATCH_PORT_GET:
                BATCH_PORT_GET:
                CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
                unlock();
                continue;
            }
            // åƒä¼ æ¨¡å¼è°ƒåº¦é€»è¾‘
            else if (m_state == MASTERSTATE::RUNNING_CONTINUOUS_TRANSFER) {
@@ -1064,15 +1058,17 @@
                }
                // Measurement -> LoadPort
                for (int s = 0; s < 4; s++) {
                    PortType pt = pLoadPorts[s]->getPortType();
                for (int p = 0; p < 4; p++) {
                    if (p != m_nContinuousWorkingPort) continue;
                    PortType pt = pLoadPorts[p]->getPortType();
                    if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_Measurement)
                        && !rmd.armState[0] && pLoadPorts[s]->isEnable()
                        && !rmd.armState[0] && pLoadPorts[p]->isEnable()
                        && (pt == PortType::Unloading || pt == PortType::Both)
                        && pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
                        && pLoadPorts[p]->getPortStatus() == PORT_INUSE) {
                        for (int slot = 0; slot < SLOT_MAX; slot++) {
                            if (slot != m_nContinuousWorkingSlot) continue;
                            m_pActiveRobotTask = createTransferTask_continuous_transfer(pMeasurement,
                                0, pLoadPorts[s], slot);
                                0, pLoadPorts[p], slot);
                            if (m_pActiveRobotTask != nullptr) {
                                m_nContinuousTransferStep = CTStep_Measurement_LoadPort;
                                m_nContinuousTransferStep = CTStep_end;
@@ -1204,17 +1200,19 @@
                }
                // LoadPort -> Aligner
                for (int s = 0; s < 4; s++) {
                    PortType pt = pLoadPorts[s]->getPortType();
                for (int p = 0; p < 4; p++) {
                    PortType pt = pLoadPorts[p]->getPortType();
                    if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_end)
                        && !rmd.armState[0] && pLoadPorts[s]->isEnable()
                        && !rmd.armState[0] && pLoadPorts[p]->isEnable()
                        && (pt == PortType::Loading || pt == PortType::Both)
                        && pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
                        && pLoadPorts[p]->getPortStatus() == PORT_INUSE) {
                        for (int slot = 0; slot < SLOT_MAX; slot++) {
                            m_pActiveRobotTask = createTransferTask_continuous_transfer(pLoadPorts[s],
                            m_pActiveRobotTask = createTransferTask_continuous_transfer(pLoadPorts[p],
                                slot, pAligner, 0);
                            if (m_pActiveRobotTask != nullptr) {
                                m_nContinuousTransferStep = CTStep_LoadPort_Aligner;
                                m_nContinuousWorkingPort = p;
                                m_nContinuousWorkingSlot = slot;
                                LOGI("<ContinuousTransfer>千传测试,开始搬送任务(LoadPort -> Aligner)...");
                                pEFEM->setContext(m_pActiveRobotTask->getContext());
                                goto CT_PORT_GET;
@@ -1498,6 +1496,8 @@
                                if (m_listener.onCjEnd != nullptr) {
                                    m_listener.onCjEnd(this, pJob);
                                }
                                completeControlJob();
                            }
                        }
                    }
@@ -1536,8 +1536,20 @@
                unlock();
            }
        };
        listener.onProcessStateChanged = [&](void* pEquipment, PROCESS_STATE state) -> void {
        listener.onProcessStateChanged = [&](void* pEquipment, int slotNo, PROCESS_STATE state) -> void {
            ASSERT(1 <= slotNo && slotNo <= 8);
            int eqid = ((CEquipment*)pEquipment)->getID();
            CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(slotNo);
            LOGI("<Master>onProcessStateChanged<%d>", (int)state);
            if (state == PROCESS_STATE::Processing) {
                if (pGlass != nullptr) {
                    m_pCollector->batchStart(eqid,
                        pGlass->getID().c_str(), 10 * 60 * 1000ULL);
                }
            }
            else if (state == PROCESS_STATE::Complete) {
                m_pCollector->batchStop(eqid);
            }
        };
        listener.onMapMismatch = [&](void* pEquipment, short scanMap, short downMap) {
            LOGE("<Master-%s>Port InUse, map(%d!=%d)不一致,请检查。",
@@ -1551,17 +1563,58 @@
                for (auto pj : pjs) {
                    auto carrier = pj->getCarrier(pPort->getCassetteId());
                    if (carrier != nullptr) {
                        carrier->contexts.clear();
                        for (auto slot : carrier->slots) {
                            CGlass* pGlass = pPort->getGlassFromSlot(slot);
                            carrier->contexts.push_back((void*)pGlass);
                            if (pGlass != nullptr) {
                                pGlass->setProcessJob(pj);
                                PJWarp& jpWarp = pj->getPjWarp();
                                int nRecipeID = RecipeManager::getInstance().getIdByPPID(pj->recipeSpec());
                                RecipeInfo stRecipeInfo = RecipeManager::getInstance().getRecipeByPPID(pj->recipeSpec());
                                std::vector<DeviceRecipe> vecRecipeInfo = stRecipeInfo.vecDeviceList;
                                pGlass->setScheduledForProcessing(jpWarp.checkSlot[slot-1]);
                                pGlass->setType(static_cast<SERVO::MaterialsType>(jpWarp.material[slot-1]));
                                SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
                                if (pJobDataS != nullptr) {
                                    SERVO::CJobDataS* pJobDataS = pGlass->getJobDataS();
                                    pJobDataS->setLotId(pj->getLotId().c_str());
                                    pJobDataS->setProductId(pj->getProductId().c_str());
                                    pJobDataS->setOperationId(pj->getOperationId().c_str());
                                    pJobDataS->setMaterialsType(jpWarp.material[slot - 1]);
                                    pJobDataS->setMasterRecipe(nRecipeID);
                                    for (const auto& info : vecRecipeInfo) {
                                        const std::string& name = info.strDeviceName;
                                        short nRecipeID = (short)info.nRecipeID;
                                        if (name == EQ_NAME_EFEM) {
                                            pJobDataS->setDeviceRecipeId(0, nRecipeID);
                                        }
                                        else if (name == EQ_NAME_BONDER1) {
                                            pJobDataS->setDeviceRecipeId(1, nRecipeID);
                                        }
                                        else if (name == EQ_NAME_BONDER2) {
                                            pJobDataS->setDeviceRecipeId(2, nRecipeID);
                                        }
                                        else if (name == EQ_NAME_BAKE_COOLING) {
                                            pJobDataS->setDeviceRecipeId(3, nRecipeID);
                                        }
                                        else if (name == EQ_NAME_VACUUMBAKE) {
                                            pJobDataS->setDeviceRecipeId(4, nRecipeID);
                                        }
                                        else if (name == EQ_NAME_MEASUREMENT) {
                                            pJobDataS->setDeviceRecipeId(5, nRecipeID);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            
            if (m_listener.onLoadPortStatusChanged != nullptr) {
@@ -1574,6 +1627,81 @@
            std::vector<CParam> params;
            ((CEquipment*)pEquipment)->parsingSVData((const char*)rawData.data(), rawData.size(), params);
        
            // ä»¥ä¸‹åŠ å…¥åˆ°æ›²çº¿æ•°æ®ä¸­
            const int64_t ts = now_ms_epoch();
            int eqid = ((CEquipment*)pEquipment)->getID();
            if (eqid == EQ_ID_Bonder1 || eqid == EQ_ID_Bonder2) {
                // å®šä¹‰ Bonder çš„特定映射
                std::vector<std::pair<int, int>> bonderMapping = {
                    {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7},
                    {8, 8}, {9, 9}, {10, 10}, {11, 11}, {12, 12}, {13, 13}, {14, 14}, {15, 15}, {16, 16}
                };
                CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
                auto& dataTypes = CServoUtilsTool::getEqDataTypes();
                auto& bonderTypes = dataTypes[eqid];
                for (const auto& mapping : bonderMapping) {
                    int paramIndex = mapping.first;
                    int channel = mapping.second;
                    if (paramIndex < params.size() && channel - 1 < bonderTypes.size()) {
                        if(m_pCollector != nullptr)
                            m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
                        if(pGlass != nullptr)
                            pGlass->addSVData(eqid, bonderTypes[channel], ts, params.at(paramIndex).getDoubleValue());
                    }
                }
            }
            else if (eqid == EQ_ID_VACUUMBAKE) {
                // å®šä¹‰ VACUUMBAKE çš„特定映射
                std::vector<std::pair<int, int>> vacuumMapping = {
                    {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7},
                    {10, 8}, {11, 9}, {12, 10}, {13, 11}, {14, 12}, {15, 13}, {16, 14}
                };
                CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
                auto& dataTypes = CServoUtilsTool::getEqDataTypes();
                auto& vacuumbakeTypes = dataTypes[eqid];
                for (const auto& mapping : vacuumMapping) {
                    int paramIndex = mapping.first;
                    int channel = mapping.second;
                    if (paramIndex < params.size() && channel - 1 < vacuumbakeTypes.size()) {
                        if (m_pCollector != nullptr)
                            m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
                        if (pGlass != nullptr)
                            pGlass->addSVData(eqid, vacuumbakeTypes[channel], ts, params.at(paramIndex).getDoubleValue());
                    }
                }
            }
            else if (eqid == EQ_ID_BAKE_COOLING) {
                // å®šä¹‰ BAKE_COOLING çš„特定映射
                std::vector<std::pair<int, int>> coolingMapping = {
                    {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6},
                    {11, 7}, {12, 8}, {13, 9}, {14, 10}, {15, 11}, {16, 12}
                };
                CGlass* pGlass = ((CEquipment*)pEquipment)->getGlassFromSlot(0);
                auto& dataTypes = CServoUtilsTool::getEqDataTypes();
                auto& coolingTypes = dataTypes[eqid];
                for (const auto& mapping : coolingMapping) {
                    int paramIndex = mapping.first;
                    int channel = mapping.second;
                    if (paramIndex < params.size() && channel - 1 < coolingTypes.size()) {
                        if (m_pCollector != nullptr)
                            m_pCollector->buffersPush(eqid, channel, ts, params.at(paramIndex).getDoubleValue());
                        if (pGlass != nullptr)
                            pGlass->addSVData(eqid, coolingTypes[channel], ts, params.at(paramIndex).getDoubleValue());
                    }
                }
            }
            // ä»¥ä¸‹æ˜¯è¾“出测试
            std::string strOut;
            char szBuffer[256];
            for (auto p : params) {
@@ -1847,6 +1975,7 @@
        // æ¨¡æ‹Ÿæµ‹è¯•
        /*
        static int aaa = 0;
        aaa++;
        if (aaa % 30 == 0) {
@@ -1900,7 +2029,7 @@
                }
            }
        }
        */
    }
    void CMaster::connectEquipments()
@@ -2285,11 +2414,19 @@
        return 0;
    }
    void CMaster::setPortType(unsigned int index, BOOL enable, int type, int mode,
        int cassetteType, int transferMode, BOOL autoChangeEnable)
    void CMaster::setPortType(unsigned int index, int type)
    {
        ASSERT(index < 4);
        int eqid[] = { EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4};
        CLoadPort* pPort = (CLoadPort*)getEquipment(eqid[index]);
        pPort->localSetPortType((SERVO::PortType)type);
    }
    void CMaster::setPortTypeEx(unsigned int index, BOOL enable, int type, int mode,
        int cassetteType, int transferMode, BOOL autoChangeEnable)
    {
        ASSERT(index < 4);
        int eqid[] = { EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4 };
        CLoadPort* pPort = (CLoadPort*)getEquipment(eqid[index]);
        pPort->localEanblePort(enable);
        pPort->localSetPortType((SERVO::PortType)type);
@@ -2403,8 +2540,31 @@
                temp.push_back(p);
            }
        }
        m_processJobs = temp;
        // æ›´æ–°context
        std::vector<uint8_t> newSlots;
        std::vector<void*> newContexts;
        for (auto pj : m_processJobs) {
            for (auto& c : pj->carriers()) {
                auto pPort = getPortWithCarrierId(c.carrierId);
                if (pPort == nullptr) continue;
                for (auto s : c.slots) {
                    auto pGlass = pPort->getGlassFromSlot(s);
                    if (pGlass == nullptr) continue;
                    newSlots.push_back(s);
                    newContexts.push_back(pGlass);
                }
                pj->setCarrierSlotsAndContexts(c.carrierId, newSlots, newContexts);
            }
        }
        this->saveState();
        return (int)m_processJobs.size();
@@ -2547,13 +2707,9 @@
        return true;
    }
    bool CMaster::loadState(const std::string& path)
    bool CMaster::loadState()
    {
        // ä¿å­˜æ–‡ä»¶è·¯å¾„
        m_strStatePath = path;
        std::ifstream ifs(path, std::ios::binary);
        std::ifstream ifs(m_strStatePath, std::ios::binary);
        if (!ifs) return false;
        // æ–‡ä»¶å¤´
@@ -2606,10 +2762,33 @@
        m_pControlJob->setPJs(tempPjs);
        // æ›´æ–°contexts
        auto pjs = m_pControlJob->getPjs();
        for (auto pj : pjs) {
            for (auto& c : pj->carriers()) {
                auto p = getPortWithCarrierId(c.carrierId);
                if (p == nullptr) continue;
                std::vector<void*> contexts;
                for (auto s : c.slots) {
                    auto g = getGlass(p->getIndex(), s - 1);
                    if (g == nullptr) continue;
                    contexts.push_back(g);
                }
                pj->setCarrierContexts(c.carrierId, contexts);
            }
        }
        // å¦‚果版本升级,可在这里判断 version æ¥åŠ è½½æ–°å­—æ®µ
        return true;
    }
    void CMaster::setStateFile(const std::string& path)
    {
        m_strStatePath = path;
    }
    CProcessJob* CMaster::acquireNextProcessJob()
@@ -2648,6 +2827,7 @@
        int nCount = 0;
        for (auto* pj : m_inProcesJobs) {
            // éåކ PJ çš„ carriers å’Œ slots
            if (pj->carriers().empty()) continue;
            for (auto& cs : pj->carriers()) {
                for (auto ctx : cs.contexts) {
                    CGlass* pGlass = (CGlass*)ctx;
@@ -2752,6 +2932,94 @@
        return nullptr;
    }
    bool CMaster::completeControlJob()
    {
        if (m_pControlJob == nullptr) {
            return false;
        }
        for (auto item : m_processJobs) {
            if (item->state() != PJState::Completed) return false;
        }
        if (m_pControlJob->state() != CJState::Completed)
            return false;
        // é‡Šæ”¾Job相关
        for (auto item : m_processJobs) {
            delete item;
        }
        m_processJobs.clear();
        if (m_pControlJob != nullptr) {
            delete m_pControlJob;
            m_pControlJob = nullptr;
        }
        // æ³¨æ„è¦é‡Šæ”¾å¼•用
        m_inProcesJobs.clear();
        m_completeProcessJobs.clear();
        m_queueGlasses.clear();
        m_inProcesGlasses.clear();
        m_completeGlasses.clear();
        saveState();
        return true;
    }
    bool CMaster::forceCompleteControlJob(std::string description)
    {
        if (m_pControlJob == nullptr || m_state != SERVO::MASTERSTATE::READY) {
            return false;
        }
        for (auto item : m_processJobs) {
            item->abort(description);
        }
        m_pControlJob->abort(description);
        // é‡Šæ”¾Job相关
        for (auto item : m_processJobs) {
            delete item;
        }
        m_processJobs.clear();
        if (m_pControlJob != nullptr) {
            delete m_pControlJob;
            m_pControlJob = nullptr;
        }
        // æ³¨æ„è¦é‡Šæ”¾å¼•用
        m_inProcesJobs.clear();
        m_completeProcessJobs.clear();
        m_queueGlasses.clear();
        m_inProcesGlasses.clear();
        m_completeGlasses.clear();
        saveState();
        return true;
    }
    bool CMaster::canCreateControlJob()
    {
        return m_pControlJob == nullptr;
    }
    bool CMaster::canCompleteControlJob()
    {
        return m_pControlJob != nullptr && m_state == SERVO::MASTERSTATE::READY;
    }
    bool CMaster::canDeleteControlJob()
    {
        return m_pControlJob != nullptr
            && m_pControlJob->state() == CJState::NoState
            && m_state == SERVO::MASTERSTATE::READY;
    }
    int CMaster::getWipGlasses(std::vector<CGlass*>& glasses)
    {
        for (auto eq : m_listEquipment) {
@@ -2826,4 +3094,71 @@
        return true;
    }
    CGlass* CMaster::getGlass(int scrPort, int scrSlot)
    {
        for (auto eq : m_listEquipment) {
            std::vector<CGlass*> glasses;
            eq->getAllGlass(glasses);
            for (auto g : glasses) {
                int p, s;
                g->getOrginPort(p, s);
                if (p == scrPort && s == scrSlot) {
                    return g;
                }
            }
        }
        return nullptr;
    }
    void CMaster::CreateDAQBridgeServer()
    {
        auto connectionStatusCallback = [&](int code, const std::string& status) {
            LOGI("<DAQBridge>status:", status.c_str());
        };
        auto rawDataCallback = [](const std::vector<uint8_t>& bytes) {
        };
        // äº‹ä»¶ï¼šæœ‰äººè¿žå…¥/断开就上日志
        auto clieintEventCallback = [](const std::string& ip, uint16_t port, bool connected) {
            LOGI("<DAQBridge>[Client %s] %s:%u", connected ? _T("JOIN") : _T("LEAVE"), ip.c_str(), port);
        };
        if (m_pCollector == nullptr) {
            m_pCollector = new Collector();
            m_pCollector->setConnectionStatusCallback(connectionStatusCallback);
            m_pCollector->setRawDataCallback(rawDataCallback);
            m_pCollector->setClientEventCallback(clieintEventCallback);
            m_pCollector->createServer(8081);
            m_pCollector->startLoop(10);
            // 1) æ³¨å†Œæœºå°ï¼ˆæŽ¨èï¼šå…ˆæ³¨å†Œ id + æœºå™¨åç§°ï¼‰
            RetentionPolicy defP; defP.mode = RetainMode::ByCount; defP.maxSamples = 200;
            m_pCollector->registryAddMachine(EQ_ID_Bonder1, "Bonder1", defP);
            m_pCollector->registryAddMachine(EQ_ID_Bonder2, "Bonder2", defP);
            m_pCollector->registryAddMachine(EQ_ID_VACUUMBAKE, "前烘烤", defP);
            m_pCollector->registryAddMachine(EQ_ID_BAKE_COOLING, "烘烤冷却", defP);
            // 2) ä¸ºé€šé“设置“曲线名称”
            auto& dataTypes = CServoUtilsTool::getEqDataTypes();
            auto& bonderTypes = dataTypes[EQ_ID_Bonder1];
            for (size_t i = 0; i < bonderTypes.size(); ++i) {
                m_pCollector->buffersSetChannelName(EQ_ID_Bonder1, i + 1, bonderTypes[i].c_str());
                m_pCollector->buffersSetChannelName(EQ_ID_Bonder2, i + 1, bonderTypes[i].c_str());
            }
            auto& vacuumbakeTypes = dataTypes[EQ_ID_VACUUMBAKE];
            for (size_t i = 0; i < vacuumbakeTypes.size(); ++i) {
                m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, i + 1, vacuumbakeTypes[i].c_str());
            }
            auto& coolingTypes = dataTypes[EQ_ID_BAKE_COOLING];
            for (size_t i = 0; i < coolingTypes.size(); ++i) {
                m_pCollector->buffersSetChannelName(EQ_ID_VACUUMBAKE, i + 1, coolingTypes[i].c_str());
            }
        }
    }
}
SourceCode/Bond/Servo/CMaster.h
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <list>
#include "CEquipment.h"
#include "CEFEM.h"
@@ -15,6 +15,7 @@
#include "CRobotTask.h"
#include "ProcessJob.h"
#include "CControlJob.h"
#include "../DAQBridge/core/Collector.h"
#define CTStep_Unknow                   0
@@ -107,8 +108,9 @@
        int abortCurrentTask();
        int restoreCurrentTask();
        int resendCurrentTask();
        void setPortType(unsigned int index, BOOL enable, int type, int mode,
        void setPortTypeEx(unsigned int index, BOOL enable, int type, int mode,
            int cassetteType, int transferMode, BOOL autoChangeEnable);
        void setPortType(unsigned int index, int type);
        void setPortCassetteType(unsigned int index, SERVO::CassetteType type);
        void setPortEnable(unsigned int index, BOOL bEnable);
        void setCompareMapsBeforeProceeding(BOOL bCompare);
@@ -128,13 +130,15 @@
        CControlJob* getControlJob();
        CLoadPort* getPortWithCarrierId(const std::string& carrierId) const;
        bool saveState() const;
        bool loadState(const std::string& path);
        bool loadState();
        void setStateFile(const std::string& path);
        int getWipGlasses(std::vector<CGlass*>& glasses);
        void test();
        bool moveGlassToBuf(int eqid, int slotNo);
        bool moveGlassToSlot(int eqid, int slotNo);
        int getPortCassetteSnSeed(int port);
        void setPortCassetteSnSeed(int port, int seed);
        CGlass* getGlass(int scrPort, int scrSlot);
    private:
        inline void lock() { EnterCriticalSection(&m_criticalSection); }
@@ -167,7 +171,7 @@
            CEquipment* pTarEq, int nTarSlot, int armNo = 1);
    public:
        // â€”— IResourceView è¦†å†™ â€”—(注意 const)
        // â€”— IResourceView è¦†å†™ â€”—(注意 const)
        bool isProcessJobsEmpty() const override;
        bool recipeExists(const std::string& ppid) const override;
        bool carrierPresent(const std::string& carrierId) const override;
@@ -179,7 +183,7 @@
        std::string& getLastErrorText();
    public:
        // æ–°å¢žå‡½æ•°
        // æ–°å¢žå‡½æ•°
        CProcessJob* acquireNextProcessJob();
        CGlass* acquireNextGlass();
        int acquireGlassToQueue();
@@ -190,7 +194,14 @@
        bool checkAndUpdatePjComplete(CProcessJob* pJob);
        bool checkAndUpdateCjComplete(CControlJob* pJob);
        CProcessJob* getGlassProcessJob(CGlass* pGlass);
        bool completeControlJob();
        bool forceCompleteControlJob(std::string description);
        bool canCreateControlJob();
        bool canCompleteControlJob();
        bool canDeleteControlJob();
        // DAQ Bridge鐩稿叧
        Collector* getCollector() const { return m_pCollector; }
    private:
        CRITICAL_SECTION m_criticalSection;
@@ -203,40 +214,42 @@
        bool m_bBatch;
    private:
        /* ç›‘控比特位的线程*/
        /* ç›‘控比特位的线程*/
        HANDLE m_hEventReadBitsThreadExit[2];
        HANDLE m_hReadBitsThreadHandle;
        unsigned m_nReadBitsThreadAddr;
        // è°ƒåº¦çº¿ç¨‹
        // è°ƒåº¦çº¿ç¨‹
        HANDLE m_hDispatchEvent;
        HANDLE m_hEventDispatchThreadExit[2];
        HANDLE m_hDispatchThreadHandle;
        unsigned m_nDispatchThreadAddr;
        // å¯åŠ¨æ—¶é—´ï¼Œè¿è¡Œæ—¶é—´ï¼ŒçŠ¶æ€
        // å¯åŠ¨æ—¶é—´ï¼Œè¿è¡Œæ—¶é—´ï¼ŒçŠ¶æ€
        ULONGLONG m_ullStartTime;
        ULONGLONG m_ullRunTime;
        MASTERSTATE m_state;
        // å½“前任务和已完成任务列表
        // å½“前任务和已完成任务列表
        CRobotTask* m_pActiveRobotTask;
        std::list< CRobotTask* > m_listTask;
        // é”™è¯¯ä»£ç 
        // é”™è¯¯ä»£ç 
        int m_nLastError;
        std::string m_strLastError;
        // åœ¨å¼€å§‹å·¥è‰ºå‰æ˜¯å¦å…ˆéœ€è¦å…ˆæ¯”较map
        // åœ¨å¼€å§‹å·¥è‰ºå‰æ˜¯å¦å…ˆéœ€è¦å…ˆæ¯”较map
        BOOL m_isCompareMapsBeforeProceeding;
        BOOL m_bJobMode;
        // åƒä¼ åœˆæ•°è®¡æ•°
        // åƒä¼ åœˆæ•°è®¡æ•°
        int m_nContinuousTransferCount;
        int m_nContinuousTransferStep;
        int m_nContinuousWorkingPort;
        int m_nContinuousWorkingSlot;
        // æ–°å¢žå·²ç»å¼€å§‹å¤„理的ProcessJob列表
        // æ–°å¢žå·²ç»å¼€å§‹å¤„理的ProcessJob列表
        std::vector<CProcessJob*> m_inProcesJobs;
        std::vector<CProcessJob*> m_completeProcessJobs;
        std::vector<CGlass*> m_queueGlasses;
@@ -252,6 +265,10 @@
        int m_nTestFlag;
        std::list<CGlass*> m_bufGlass;
    private:
        Collector* m_pCollector = nullptr;
        void CreateDAQBridgeServer();
    };
}
SourceCode/Bond/Servo/CMeasurement.cpp
@@ -455,22 +455,37 @@
        params.push_back(CParam("检测速度", "", this->getName().c_str(), v * 0.001));
        i += 4;
        return (int)params.size();
    }
    int CMeasurement::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
    {
        ASSERT(pszData);
        if (size < 250) return 0;
        int i = 0, v;
        // 1.检测功能启用/禁用
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
        params.push_back(CParam("检测功能启用/禁用", "", this->getName().c_str(), v));
        i += 2;
        // 2.检测速度
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
        params.push_back(CParam("检测速度", "", this->getName().c_str(), v * 0.001));
        i += 4;
        // 3.检测1数据
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
        params.push_back(CParam("检测1数据", "", this->getName().c_str(), v * 0.001));
        i += 4;
        // 4.检测2数据
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
        params.push_back(CParam("检测2数据", "", this->getName().c_str(), v * 0.001));
        i += 4;
        return (int)params.size();
    }
    int CMeasurement::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
    {
        return parsingParams(pszData, size, params);
    }
    int CMeasurement::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
SourceCode/Bond/Servo/CPageCollectionEvent.cpp
@@ -106,7 +106,6 @@
    CHMPropertyPage::OnSize(nType, cx, cy);
    if (GetDlgItem(IDC_LIST1) == nullptr) return;
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -100,6 +100,9 @@
// ====== å¼€å…³ï¼š1=启用假数据(只替换 DB æŸ¥è¯¢ï¼‰ï¼›0=用真实 DB ======
#define USE_FAKE_DB_DEMO 0
// ====== å¼€å…³ï¼š1=启用模拟传感器数据生成;0=使用真实数据 ======
#define USE_MOCK_SENSOR_DATA 0
#if USE_FAKE_DB_DEMO
#include <ctime>
#include <atlconv.h>   // CStringA
@@ -372,6 +375,55 @@
    return true;
}
// è¾…助函数:将 ANSI CString å†™å…¥æ–‡ä»¶ä¸º UTF-8 ç¼–码
bool CPageGlassList::WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath)
{
    CFile file;
    if (!file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
        return false;
    }
    // å†™å…¥ UTF-8 BOM
    const unsigned char bom[] = { 0xEF, 0xBB, 0xBF };
    file.Write(bom, 3);
    // å°† ANSI è½¬æ¢ä¸º Unicode
    int unicodeLength = MultiByteToWideChar(CP_ACP, 0,
        ansiContent, ansiContent.GetLength(),
        NULL, 0);
    if (unicodeLength <= 0) {
        file.Close();
        return false;
    }
    wchar_t* unicodeBuffer = new wchar_t[unicodeLength + 1];
    MultiByteToWideChar(CP_ACP, 0,
        ansiContent, ansiContent.GetLength(),
        unicodeBuffer, unicodeLength);
    unicodeBuffer[unicodeLength] = 0;
    // å°† Unicode è½¬æ¢ä¸º UTF-8
    int utf8Length = WideCharToMultiByte(CP_UTF8, 0,
        unicodeBuffer, unicodeLength,
        NULL, 0, NULL, NULL);
    bool success = false;
    if (utf8Length > 0) {
        char* utf8Buffer = new char[utf8Length];
        WideCharToMultiByte(CP_UTF8, 0,
            unicodeBuffer, unicodeLength,
            utf8Buffer, utf8Length, NULL, NULL);
        file.Write(utf8Buffer, utf8Length);
        delete[] utf8Buffer;
        success = true;
    }
    delete[] unicodeBuffer;
    file.Close();
    return success;
}
// CPageGlassList å¯¹è¯æ¡†
@@ -426,6 +478,7 @@
    ON_BN_CLICKED(IDC_BUTTON_PREV_PAGE, &CPageGlassList::OnBnClickedButtonPrevPage)
    ON_BN_CLICKED(IDC_BUTTON_NEXT_PAGE, &CPageGlassList::OnBnClickedButtonNextPage)
    ON_NOTIFY(ELCN_SHOWFULLTEXT, IDC_LIST_ALARM, &CPageGlassList::OnShowFullText)
    ON_BN_CLICKED(IDC_BUTTON_EXPORT_ROW, &CPageGlassList::OnBnClickedButtonExportRow)
END_MESSAGE_MAP()
// ===== ç§æœ‰å°å·¥å…· =====
@@ -661,106 +714,163 @@
    auto pageFull = db.queryPaged(m_filters, rawLimit, rawOffset);
#endif
    // å¦‚果多出一条,看看它是否是“本页最后一条”的 buddy
    std::optional<decltype(pageFull.items)::value_type> lookahead; // é¢„读记录(若与最后一条配对)
#if !USE_FAKE_DB_DEMO
    // â€”— ä¸‰å…ƒé”®å·¥å…·ï¼š<classId>|C<cassette>|J<job> â€”— //
// â€”— ä¸‰å…ƒé”®å·¥å…·ï¼š<classId>|C<cassette>|J<job> â€”— //
    auto makeKey = [](const std::string& cls, int csn, int jsn) -> std::string {
        std::string k;
        k.reserve(cls.size() + 32);
        k.append(cls);
        k.push_back('|'); k.push_back('C');
        k.append(std::to_string(csn));
        k.push_back('|'); k.push_back('J');
        k.append(std::to_string(jsn));
        return k;
    };
    // â˜…★★ è¿™é‡Œæ˜¯å…³é”®ä¿®å¤ï¼šæŽ¥æ”¶â€œ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 +887,162 @@
        auto* nParent = m_listCtrl.InsertRoot(pcols);
        MaybeRestoreExpandByKey(nParent, expandedKeys);
        m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
        m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), zebraBk(zebra));
        std::vector<CString> ccols(colCount); // å ä½åªå†™ ClassID
        ccols[4] = r.buddyId.c_str();
        std::vector<CString> ccols(colCount);
        ccols[4] = r.buddyId.c_str(); // å ä½
        auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), zebraBk(zebra));
        consumed.insert(r.classId);
        consumed.insert(makeKeyR(r));
        ++zebra;
    }
    // -------- Phase 2: å‰©ä½™æœªæ¶ˆè´¹çš„,作为“单条根行” ----------
    for (size_t i = 0; i < page.items.size(); ++i) {
        const auto& r = page.items[i];
        if (consumed.count(r.classId)) continue;
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(makeKeyR(r))) continue;
        COLORREF bk = zebraBk(zebra);
        std::vector<CString> cols(colCount);
        cols[1] = std::to_string(r.id).c_str();
        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
        cols[3] = std::to_string(r.jobSeqNo).c_str();
        cols[4] = r.classId.c_str();
        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
        cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
        cols[7] = r.tStart.c_str();
        cols[8] = r.tEnd.c_str();
        cols[9] = r.buddyId.c_str();
        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
        cols[11] = r.path.c_str();
        cols[12] = r.params.c_str();
        auto* n = m_listCtrl.InsertRoot(cols);
        m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
        consumed.insert(makeKeyR(r));
        ++zebra;
    }
    // ä¸€æ¬¡æ€§é‡ç»˜
    m_listCtrl.RebuildVisible();
#else
    // ===== DEMO åˆ†æ”¯ï¼ˆä¿æŒåŽŸæ ·ï¼›è‹¥è¦æ¼”ç¤ºåŒæ ·é€»è¾‘ï¼Œå¯ä»¿ç…§ä¸Šé¢æ”¹é€ ï¼‰=====
    // å¦‚果多出一条,看看它是否是“本页最后一条”的 buddy
    std::optional<decltype(page.items)::value_type> lookahead;
    auto iEquals = [](const std::string& a, const std::string& b) {
#ifdef _WIN32
        return _stricmp(a.c_str(), b.c_str()) == 0;
#else
        return strcasecmp(a.c_str(), b.c_str()) == 0;
#endif
    };
    if (page.items.size() == rawLimit) {
        const auto& last = page.items[PAGE_SIZE - 1];
        const auto& extra = page.items[PAGE_SIZE];
        bool pair =
            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) ||
            (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId));
        if (pair) lookahead = extra;
        page.items.pop_back();
    }
    // ä½ å¯ä»¥æŠŠ DEMO åˆ†æ”¯ä¹Ÿåˆ‡åˆ°ä¸‰å…ƒé”®é€»è¾‘;这里从略
    auto& pageRef = page;
    std::unordered_map<std::string, size_t> idxById;
    idxById.reserve(pageRef.items.size());
    for (size_t i = 0; i < pageRef.items.size(); ++i) idxById[pageRef.items[i].classId] = i;
    std::unordered_set<std::string> consumed;
    int zebra = 0;
    auto zebraBk = [&](int z) -> COLORREF {
        return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
    };
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(r.classId)) continue;
        if (!r.buddyId.empty()) {
            auto it = idxById.find(r.buddyId);
            if (it != idxById.end()) {
                const auto& br = pageRef.items[it->second];
                if (!consumed.count(br.classId)) {
                    COLORREF bk = zebraBk(zebra);
                    std::vector<CString> pcols(colCount), ccols(colCount);
                    pcols[1] = std::to_string(r.id).c_str();
                    pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
                    pcols[3] = std::to_string(r.jobSeqNo).c_str();
                    pcols[4] = r.classId.c_str();
                    pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
                    pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
                    pcols[7] = r.tStart.c_str();
                    pcols[8] = r.tEnd.c_str();
                    pcols[9] = r.buddyId.c_str();
                    pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
                    pcols[11] = r.path.c_str();
                    pcols[12] = r.params.c_str();
                    auto* nParent = m_listCtrl.InsertRoot(pcols);
                    MaybeRestoreExpandByKey(nParent, expandedKeys);
                    m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
                    ccols[1] = std::to_string(br.id).c_str();
                    ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
                    ccols[3] = std::to_string(br.jobSeqNo).c_str();
                    ccols[4] = br.classId.c_str();
                    ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
                    ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
                    ccols[7] = br.tStart.c_str();
                    ccols[8] = br.tEnd.c_str();
                    ccols[9] = br.buddyId.c_str();
                    ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
                    ccols[11] = br.path.c_str();
                    ccols[12] = br.params.c_str();
                    auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
                    m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
                    consumed.insert(r.classId);
                    consumed.insert(br.classId);
                    ++zebra;
                    continue;
                }
            }
            // æ’占位子
            COLORREF bk = zebraBk(zebra);
            std::vector<CString> pcols(colCount), ccols(colCount);
            pcols[1] = std::to_string(r.id).c_str();
            pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
            pcols[3] = std::to_string(r.jobSeqNo).c_str();
            pcols[4] = r.classId.c_str();
            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
            pcols[7] = r.tStart.c_str();
            pcols[8] = r.tEnd.c_str();
            pcols[9] = r.buddyId.c_str();
            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
            pcols[11] = r.path.c_str();
            pcols[12] = r.params.c_str();
            auto* nParent = m_listCtrl.InsertRoot(pcols);
            MaybeRestoreExpandByKey(nParent, expandedKeys);
            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
            ccols[4] = r.buddyId.c_str();
            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
            consumed.insert(r.classId);
            ++zebra;
        }
    }
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(r.classId)) continue;
        COLORREF bk = zebraBk(zebra);
        std::vector<CString> cols(colCount);
        cols[1] = std::to_string(r.id).c_str();
        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
@@ -816,8 +1064,8 @@
        ++zebra;
    }
    // ä¸€æ¬¡æ€§é‡ç»˜
    m_listCtrl.RebuildVisible();
#endif
    // ä¸Šä¸€é¡µ / ä¸‹ä¸€é¡µ
    UpdatePageControls();
@@ -1045,6 +1293,274 @@
    }
}
void CPageGlassList::OnBnClickedButtonExportRow()
{
    int nSelected = m_listCtrl.GetSelectionMark();
    if (nSelected == -1) {
        AfxMessageBox(_T("请先选择一行记录!"));
        return;
    }
    // ç›´æŽ¥ä»Žç¬¬ä¸€åˆ—获取 ID
    CString strId = m_listCtrl.GetItemText(nSelected, 1);
    if (strId.IsEmpty()) {
        AfxMessageBox(_T("WIP记录暂不支持保存"));
        return;
    }
    // æ•°æ®åº“记录
    long long recordId = _ttoi64(strId);
    // ä»Žæ•°æ®åº“查询完整记录
    auto& db = GlassLogDb::Instance();
    auto row = db.queryById(recordId);
    if (!row) {
        AfxMessageBox(_T("查询记录失败"));
        return;
    }
    // ä½¿ç”¨ Glass ID æž„建默认文件名
    CString strDefaultFileName;
    CString strGlassId = row->classId.c_str();
    // ç§»é™¤æ–‡ä»¶åä¸­çš„非法字符
    CString strSanitizedGlassId = strGlassId;
    strSanitizedGlassId.Remove('\\');
    strSanitizedGlassId.Remove('/');
    strSanitizedGlassId.Remove(':');
    strSanitizedGlassId.Remove('*');
    strSanitizedGlassId.Remove('?');
    strSanitizedGlassId.Remove('"');
    strSanitizedGlassId.Remove('<');
    strSanitizedGlassId.Remove('>');
    strSanitizedGlassId.Remove('|');
    strDefaultFileName.Format(_T("Glass_%s.csv"), strSanitizedGlassId);
    // æ–‡ä»¶ä¿å­˜å¯¹è¯æ¡†ï¼Œè®¾ç½®é»˜è®¤æ–‡ä»¶å
    CFileDialog fileDialog(FALSE, _T("csv"), strDefaultFileName,
        OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
        _T("CSV Files (*.csv)|*.csv|JSON Files (*.json)|*.json||"));
    if (fileDialog.DoModal() != IDOK) return;
    CString filePath = fileDialog.GetPathName();
    CString fileExt = fileDialog.GetFileExt();
    if (fileExt.CompareNoCase(_T("json")) == 0) {
        ExportToJson(*row, filePath);
    }
    else {
        ExportToCsv(*row, filePath);
    }
}
void CPageGlassList::ExportToJson(const GlassLogDb::Row& row, const CString& filePath)
{
    // ä¿å­˜ä¸º JSON
    if (!row.pretty.empty()) {
        CFile file;
        if (file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
            file.Write(row.pretty.c_str(), row.pretty.length());
            file.Close();
            CString strSuccess;
            strSuccess.Format(_T("记录已保存为JSON文件:\n%s"), filePath);
            AfxMessageBox(strSuccess);
        }
        else {
            AfxMessageBox(_T("保存文件失败"));
        }
    }
    else {
        AfxMessageBox(_T("该记录没有JSON数据"));
    }
}
void CPageGlassList::ExportToCsv(const GlassLogDb::Row& row, const CString& filePath)
{
    CString csvContent;
    // === ç¬¬ä¸€éƒ¨åˆ†ï¼šåŸºç¡€ä¿¡æ¯ ===
    ExportBasicInfo(csvContent, row);
    // === ç¬¬äºŒéƒ¨åˆ†ï¼šå·¥è‰ºå‚æ•° ===
    ExportProcessParams(csvContent, row);
    // === ç¬¬ä¸‰éƒ¨åˆ†ï¼šä¼ æ„Ÿå™¨æ•°æ®è¯¦æƒ… ===
    ExportSensorData(csvContent, row);
    // ä½¿ç”¨è¾…助函数保存为 UTF-8 ç¼–码
    if (WriteAnsiStringAsUtf8ToFile(csvContent, filePath)) {
        CString strSuccess;
        strSuccess.Format(_T("记录已保存为CSV文件:\n%s"), filePath);
        AfxMessageBox(strSuccess);
    }
    else {
        AfxMessageBox(_T("保存文件失败"));
    }
}
void CPageGlassList::ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("=== åŸºç¡€ä¿¡æ¯ ===\n");
    csvContent += _T("ID,Cassette序列号,Job序列号,Glass ID,物料类型,状态,开始时间,结束时间,绑定Glass ID,AOI结果,路径\n");
    CString baseInfoRow;
    baseInfoRow.Format(_T("%lld,%d,%d,%s,%d,%d,%s,%s,%s,%d,%s\n"),
        row.id, row.cassetteSeqNo, row.jobSeqNo,
        CString(row.classId.c_str()), row.materialType, row.state,
        CString(row.tStart.c_str()), CString(row.tEnd.c_str()),
        CString(row.buddyId.c_str()), row.aoiResult,
        CString(row.path.c_str()));
    csvContent += baseInfoRow;
}
void CPageGlassList::ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("\n=== å·¥è‰ºå‚æ•° ===\n");
    // å¦‚果有 pretty å­—段,解析工艺参数
    if (!row.pretty.empty()) {
        SERVO::CGlass tempGlass;
        if (GlassJson::FromString(row.pretty, tempGlass)) {
            auto& params = tempGlass.getParams();
            if (!params.empty()) {
                // å·¥è‰ºå‚数表头
                csvContent += _T("参数名称,参数ID,数值,机器单元\n");
                // å·¥è‰ºå‚数数据
                for (auto& param : params) {
                    CString paramRow;
                    CString valueStr;
                    // æ ¹æ®å‚数类型格式化数值
                    if (param.getValueType() == PVT_INT) {
                        valueStr.Format(_T("%d"), param.getIntValue());
                    }
                    else {
                        valueStr.Format(_T("%.3f"), param.getDoubleValue());
                    }
                    paramRow.Format(_T("%s,%s,%s,%s\n"),
                        CString(param.getName().c_str()),
                        CString(param.getId().c_str()),
                        valueStr,
                        CString(param.getUnit().c_str()));
                    csvContent += paramRow;
                }
            }
            else {
                csvContent += _T("无工艺参数数据\n");
            }
        }
        else {
            csvContent += _T("无法解析工艺参数\n");
        }
    }
    else {
        csvContent += _T("无工艺参数数据\n");
    }
}
void CPageGlassList::ExportSensorData(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("\n=== ä¼ æ„Ÿå™¨æ•°æ®è¯¦æƒ… ===\n");
    // å¦‚果有 pretty å­—段,解析传感器数据
    if (!row.pretty.empty()) {
        SERVO::CGlass tempGlass;
        if (GlassJson::FromString(row.pretty, tempGlass)) {
#if USE_MOCK_SENSOR_DATA
            // ç”Ÿæˆæ¨¡æ‹Ÿçš„SVData用于测试
            GenerateMockSVData(tempGlass);
#endif
            // å¯¹æ¯ä¸ªæœºå™¨ç”Ÿæˆè¡¨æ ¼
            for (const auto& machinePair : tempGlass.getAllSVData()) {
                int machineId = machinePair.first;
                CString machineName = CString(SERVO::CServoUtilsTool::getEqName(machineId).c_str());
                csvContent += _T("\n[") + machineName + _T("]\n");
                // èŽ·å–è¯¥æœºå™¨çš„é¢„å®šä¹‰åˆ—é¡ºåº
                auto columnOrder = getMachineColumnOrder(machineId);
                if (columnOrder.empty()) {
                    csvContent += _T("无预定义列配置\n");
                    continue;
                }
                // æž„建表头 - ç›´æŽ¥ä½¿ç”¨ä¸­æ–‡åˆ—名
                CString header = _T("时间戳(ms),本地时间");
                for (const auto& dataType : columnOrder) {
                    header += _T(",");
                    header += CString(dataType.c_str()); // ç›´æŽ¥ä½¿ç”¨ä¸­æ–‡åˆ—名
                }
                header += _T("\n");
                csvContent += header;
                // æ£€æŸ¥æ˜¯å¦æœ‰æ•°æ®
                if (machinePair.second.empty()) {
                    csvContent += _T("无传感器数据\n");
                    continue;
                }
                // ä½¿ç”¨ç¬¬ä¸€ä¸ªæ•°æ®ç±»åž‹çš„æ—¶é—´åºåˆ—作为基准
                const std::string& firstDataType = columnOrder[0];
                auto firstDataTypeIt = machinePair.second.find(firstDataType);
                if (firstDataTypeIt == machinePair.second.end() || firstDataTypeIt->second.empty()) {
                    csvContent += _T("无基准数据类型数据\n");
                    continue;
                }
                const auto& timeSeries = firstDataTypeIt->second;
                // å¯¹äºŽæ¯ä¸ªæ—¶é—´ç‚¹ï¼Œè¾“出一行数据
                for (size_t i = 0; i < timeSeries.size(); i++) {
                    auto timestamp = timeSeries[i].timestamp;
                    // æ—¶é—´æˆ³ï¼ˆæ¯«ç§’)
                    auto ms = timePointToMs(timestamp);
                    CString row;
                    row.Format(_T("%lld,"), ms);
                    // æœ¬åœ°æ—¶é—´å­—符串
                    CString localTime = CString(timePointToString(timestamp).c_str());
                    row += localTime;
                    // æŒ‰ç…§é¢„定义的列顺序输出数据
                    for (const auto& dataType : columnOrder) {
                        row += _T(",");
                        auto dataTypeIt = machinePair.second.find(dataType);
                        if (dataTypeIt != machinePair.second.end() && i < dataTypeIt->second.size()) {
                            // ç›´æŽ¥æŒ‰ç´¢å¼•获取数据
                            CString valueStr;
                            valueStr.Format(_T("%.3f"), dataTypeIt->second[i].value);
                            row += valueStr;
                        }
                        else {
                            // ç†è®ºä¸Šä¸åº”该发生,因为您说没有空值
                            row += _T("N/A");
                        }
                    }
                    row += _T("\n");
                    csvContent += row;
                }
            }
        }
        else {
            csvContent += _T("无法解析传感器数据\n");
        }
    }
    else {
        csvContent += _T("无传感器数据\n");
    }
}
void CPageGlassList::OnBnClickedButtonPrevPage()
{
    if (m_nCurPage > 1) {
@@ -1162,7 +1678,7 @@
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) é€ä¸ªå¤„理 WIP:已存在 -> å°±åœ°æ›´æ–°ï¼›å¿…要时“只对根补子项”
    //                 ä¸å­˜åœ¨ -> ä¼˜å…ˆæŒ‚到 buddy å®¹å™¨ï¼›å¦åˆ™è§¦å‘整页重建(新根保持顶部)
    //                 ä¸å­˜åœ¨ -> æŒ‚到 buddy å®¹å™¨ï¼›è‹¥ buddy ä¸åœ¨å¯è§è¡¨ï¼Œè§¦å‘全量重建(保证 WIP é¡¶éƒ¨ï¼‰
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
@@ -1205,7 +1721,7 @@
                }
            }
            // â€”— åªå¯¹â€œæ ¹èŠ‚ç‚¹â€è¡¥å­é¡¹ï¼Œä¸”ä»…å½“ buddy å°šæœªå‡ºçŽ°åœ¨å¯è§è¡¨ï¼Œä¸”æ ¹ä¸‹ä¹Ÿæ²¡æœ‰è¯¥ buddy â€”—
            // â€”— åªå¯¹â€œæ ¹èŠ‚ç‚¹â€è¡¥å­é¡¹ â€”—
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                auto itRoot = wipRootById.find(cid);
@@ -1227,7 +1743,7 @@
                    bool buddyExistsAnywhere = (wipRowById.find(newBid) != wipRowById.end());
                    bool hasChildAlready = NodeHasChildWithClassId(container, newBuddyCid);
                    // å…³ç³»æ˜¯å¦å‘生变化?(oldChildCid ä¸Ž newBuddyCid ä¸åŒï¼Œæˆ–有子但现在没 buddy)
                    // å…³ç³»æ˜¯å¦å‘生变化?
                    bool relationChanged =
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||
@@ -1270,10 +1786,9 @@
                        }
                    }
                }
                // è‹¥å½“前是“子节点”,不在这里调整父子关系;让“关系变化”走全量重建
            }
            else {
                // æ²¡æœ‰ buddy:如果容器下现在有子,也算关系变化,触发重建
                // æ²¡ buddy ä½†å®¹å™¨ä¸‹æœ‰å­ -> å…³ç³»å˜åŒ–,触发全量重建
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
@@ -1284,8 +1799,6 @@
        }
        else {
            // (B) ä¸å­˜åœ¨ï¼šæ–°å¢ž
            //   å…ˆå°è¯•“挂到 buddy çš„容器根”下面;
            //   è‹¥ buddy ä¸åœ¨å½“前可见表,则触发全量重建(保证 WIP é¡¶éƒ¨ï¼‰ã€‚
            SERVO::CGlass* b = g->getBuddy();
            CExpandableListCtrl::Node* container = nullptr;
@@ -1303,7 +1816,6 @@
            }
            if (container) {
                // buddy å®¹å™¨å­˜åœ¨ï¼šæŠŠ g ä½œä¸ºâ€œå­è¡Œâ€æŒ‚上去(避免重复)
                CString cidCs = g->getID().c_str();
                if (!NodeHasChildWithClassId(container, cidCs)) {
                    if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
@@ -1405,3 +1917,116 @@
    return CDialogEx::PreTranslateMessage(pMsg);
}
// èŽ·å–æœºå™¨é¢„å®šä¹‰çš„åˆ—é¡ºåº
std::vector<std::string> CPageGlassList::getMachineColumnOrder(int machineId)
{
    auto dataTypes = SERVO::CServoUtilsTool::getEqDataTypes();
    auto it = dataTypes.find(machineId);
    return it != dataTypes.end() ? it->second : std::vector<std::string>();
}
// æ—¶é—´æˆ³è½¬æ¢ä¸ºå­—符串
std::string CPageGlassList::timePointToString(const std::chrono::system_clock::time_point& tp)
{
    auto time_t = std::chrono::system_clock::to_time_t(tp);
    std::tm tm;
    localtime_s(&tm, &time_t);
    char buffer[20];
    std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
    return buffer;
}
// æ—¶é—´æˆ³è½¬æ¢ä¸ºæ¯«ç§’
int64_t CPageGlassList::timePointToMs(const std::chrono::system_clock::time_point& tp)
{
    return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
}
// ç”Ÿæˆæ¨¡æ‹Ÿçš„SVData用于测试
void CPageGlassList::GenerateMockSVData(SERVO::CGlass& glass)
{
    // èŽ·å–è®¾å¤‡æ•°æ®ç±»åž‹é…ç½®
    auto& dataTypes = SERVO::CServoUtilsTool::getEqDataTypes();
    // ä¸ºæ¯ä¸ªè®¾å¤‡ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    for (const auto& machinePair : dataTypes) {
        int machineId = machinePair.first;
        const auto& dataTypeList = machinePair.second;
        // ç”Ÿæˆæ—¶é—´åºåˆ—:从当前时间往前推10分钟,每1秒一个数据点
        auto now = std::chrono::system_clock::now();
        auto startTime = now - std::chrono::minutes(10);
        // ä¸ºæ¯ä¸ªæ•°æ®ç±»åž‹ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
        for (const auto& dataType : dataTypeList) {
            std::vector<SERVO::SVDataItem> mockData;
            // ç”Ÿæˆ600个数据点(10分钟 * 60个点/分钟)
            for (int i = 0; i < 600; ++i) {
                auto timestamp = startTime + std::chrono::seconds(i * 1);
                // æ ¹æ®è®¾å¤‡ç±»åž‹å’Œæ•°æ®ç±»åž‹ç”Ÿæˆä¸åŒçš„æ¨¡æ‹Ÿå€¼
                double value = GenerateMockValue(machineId, dataType, i);
                mockData.emplace_back(timestamp, value);
            }
            // å°†æ¨¡æ‹Ÿæ•°æ®æ·»åŠ åˆ°glass对象中
            glass.addSVData(machineId, dataType, mockData);
        }
    }
}
// æ ¹æ®è®¾å¤‡ç±»åž‹å’Œæ•°æ®ç±»åž‹ç”Ÿæˆæ¨¡æ‹Ÿæ•°å€¼
double CPageGlassList::GenerateMockValue(int machineId, const std::string& dataType, int index)
{
    // åŸºç¡€å€¼èŒƒå›´
    double baseValue = 0.0;
    double variation = 0.0;
    // æ ¹æ®è®¾å¤‡ç±»åž‹è®¾ç½®åŸºç¡€å€¼
    switch (machineId) {
        case EQ_ID_Bonder1:
        case EQ_ID_Bonder2:
            if (dataType.find("压力") != std::string::npos) {
                baseValue = 50.0;  // åŽ‹åŠ›åŸºç¡€å€¼
                variation = 10.0;  // åŽ‹åŠ›å˜åŒ–èŒƒå›´
            } else if (dataType.find("温度") != std::string::npos) {
                baseValue = 180.0; // æ¸©åº¦åŸºç¡€å€¼
                variation = 5.0;   // æ¸©åº¦å˜åŒ–范围
            } else if (dataType.find("扩展值") != std::string::npos) {
                baseValue = 100.0; // æ‰©å±•值基础值
                variation = 15.0;  // æ‰©å±•值变化范围
            }
            break;
        case EQ_ID_VACUUMBAKE:
            if (dataType.find("扩展值") != std::string::npos) {
                baseValue = 80.0;
                variation = 12.0;
            } else if (dataType.find("温度") != std::string::npos) {
                baseValue = 200.0;
                variation = 8.0;
            }
            break;
        case EQ_ID_BAKE_COOLING:
            if (dataType.find("温度") != std::string::npos) {
                baseValue = 25.0;  // å†·å´æ¸©åº¦
                variation = 3.0;
            }
            break;
        default:
            baseValue = 50.0;
            variation = 5.0;
            break;
    }
    // æ·»åŠ æ—¶é—´ç›¸å…³çš„è¶‹åŠ¿å’Œéšæœºå˜åŒ–
    double timeTrend = sin(index * 0.1) * 2.0;  // æ­£å¼¦æ³¢è¶‹åŠ¿
    double randomNoise = (rand() % 100 - 50) / 100.0 * variation * 0.3;  // éšæœºå™ªå£°
    return baseValue + timeTrend + randomNoise;
}
SourceCode/Bond/Servo/CPageGlassList.h
@@ -2,6 +2,10 @@
#include "CExpandableListCtrl.h"
#include "GlassLogDb.h"
// ====== ç¼–译开关说明 ======
// USE_MOCK_SENSOR_DATA: 1=启用模拟传感器数据生成;0=使用真实数据
// åœ¨ CPageGlassList.cpp ä¸­å®šä¹‰
// CPageGlassList å¯¹è¯æ¡†
@@ -55,6 +59,17 @@
    void UpdateWipData();
    bool eraseGlassInVector(SERVO::CGlass* pGlass, std::vector<SERVO::CGlass*>& glasses);
    void UpdateWipRow(unsigned int index, SERVO::CGlass* pGlass);
    bool WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath);
    void ExportToCsv(const GlassLogDb::Row& row, const CString& filePath);
    void ExportToJson(const GlassLogDb::Row& row, const CString& filePath);
    void ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row);
    void ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row);
    void ExportSensorData(CString& csvContent, const GlassLogDb::Row& row);
    static std::vector<std::string> getMachineColumnOrder(int machineId);
    static std::string timePointToString(const std::chrono::system_clock::time_point& tp);
    static int64_t timePointToMs(const std::chrono::system_clock::time_point& tp);
    void GenerateMockSVData(SERVO::CGlass& glass);
    double GenerateMockValue(int machineId, const std::string& dataType, int index);
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
@@ -77,4 +92,6 @@
    afx_msg void OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult);
    virtual BOOL PreTranslateMessage(MSG* pMsg);
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnBnClickedButtonExportRow();
};
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/CPageReport.cpp
@@ -108,7 +108,6 @@
    CHMPropertyPage::OnSize(nType, cx, cy);
    if (GetDlgItem(IDC_LIST1) == nullptr) return;
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
SourceCode/Bond/Servo/CPageVarialbles.cpp
@@ -108,7 +108,6 @@
    if (GetDlgItem(IDC_LIST1) == nullptr) return;
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    m_listCtrl.MoveWindow(12, 12, rcClient.Width() - 24, rcClient.Height() - 24);
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/CServoUtilsTool.cpp
@@ -4,6 +4,32 @@
namespace SERVO {
    static std::unordered_map<int, std::vector<std::string>> EQ_DATA_TYPES = {
        {EQ_ID_Bonder1, {
            "气囊压力", "上腔压力", "管道真空规值", "腔体真空规值",
            "上腔温度1", "上腔温度2", "上腔温度3", "上腔温度4",
            "上腔温度5", "上腔温度6", "下腔温度1", "下腔温度2",
            "下腔温度3", "下腔温度4", "下腔温度5", "下腔温度6"
        }},
        {EQ_ID_Bonder2, {
            "气囊压力", "上腔压力", "管道真空规值", "腔体真空规值",
            "上腔温度1", "上腔温度2", "上腔温度3", "上腔温度4",
            "上腔温度5", "上腔温度6", "下腔温度1", "下腔温度2",
            "下腔温度3", "下腔温度4", "下腔温度5", "下腔温度6"
        }},
        {EQ_ID_VACUUMBAKE, {
            "A腔真空规值", "A腔温控1", "A腔温控2", "A腔温控4",
            "A腔温控5", "A腔温控6", "A腔温控7", "B腔真空规值",
            "B腔温控1", "B腔温控2", "B腔温控4", "B腔温控5",
            "B腔温控6", "B腔温控7"
        }},
        {EQ_ID_BAKE_COOLING, {
            "A烘烤温控1", "A烘烤温控2", "A烘烤温控4", "A烘烤温控5",
            "A烘烤温控6", "A烘烤温控7", "B烘烤温控1", "B烘烤温控2",
            "B烘烤温控4", "B烘烤温控5", "B烘烤温控6", "B烘烤温控7"
        }}
    };
    CServoUtilsTool::CServoUtilsTool()
    {
@@ -12,6 +38,43 @@
    CServoUtilsTool::~CServoUtilsTool()
    {
    }
    std::string CServoUtilsTool::getEqName(int eqid)
    {
        switch (eqid)
        {
        case EQ_ID_LOADPORT1:
            return "Port1";
        case EQ_ID_LOADPORT2:
            return "Port2";
        case EQ_ID_LOADPORT3:
            return "Port3";
        case EQ_ID_LOADPORT4:
            return "Port4";
        case EQ_ID_ALIGNER:
            return "Aligner";
        case EQ_ID_FLIPER:
            return "Fliper";
        case EQ_ID_VACUUMBAKE:
            return "VacuumBake";
        case EQ_ID_Bonder1:
            return "Bonder1";
        case EQ_ID_Bonder2:
            return "Bonder2";
        case EQ_ID_BAKE_COOLING:
            return "BakeCooling";
        case EQ_ID_MEASUREMENT:
            return "AOI";
        case EQ_ID_ARM_TRAY1:
            return "ArmTray1";
        case EQ_ID_ARM_TRAY2:
            return "ArmTray2";
        default:
            break;
        }
        return "";
    }
    std::string CServoUtilsTool::getEqUnitName(int eqid, int unit)
@@ -39,11 +102,6 @@
            if (unit == 1) return "烘烤B腔";
        }
        if (eqid == EQ_ID_VACUUMBAKE) {
            if (unit == 0) return "烘烤A腔";
            if (unit == 1) return "烘烤B腔";
        }
        if (eqid == EQ_ID_Bonder1) {
            return "Bonder1";
        }
@@ -56,8 +114,8 @@
            if (unit == 0) return "后烘烤A腔";
            if (unit == 1) return "冷却A";
            if (unit == 0) return "后烘烤B腔";
            if (unit == 1) return "冷却B";
            if (unit == 2) return "后烘烤B腔";
            if (unit == 3) return "冷却B";
        }
        if (eqid == EQ_ID_MEASUREMENT) {
@@ -203,4 +261,9 @@
            break;
        }
    }
    std::unordered_map<int, std::vector<std::string>>& CServoUtilsTool::getEqDataTypes()
    {
        return EQ_DATA_TYPES;
    }
}
SourceCode/Bond/Servo/CServoUtilsTool.h
@@ -11,11 +11,13 @@
        virtual ~CServoUtilsTool();
    public:
        static std::string getEqName(int eqid);
        static std::string getEqUnitName(int eqid, int unit);
        static std::string getEqUnitName(int eqid, int unit, int slot);
        static std::string getMaterialsTypeText(MaterialsType type);
        static std::string getGlassStateText(SERVO::GlsState state);
        static std::string getInspResultText(SERVO::InspResult result);
        static std::unordered_map<int, std::vector<std::string>>& getEqDataTypes();
    };
}
SourceCode/Bond/Servo/CVacuumBake.cpp
@@ -482,7 +482,58 @@
    int CVacuumBake::parsingProcessData(const char* pszData, size_t size, std::vector<CParam>& params)
    {
        return parsingParams(pszData, size, params);
        ASSERT(pszData);
        if (size < 250) return 0;
        int i = 0, v;
        // 1.工艺参数序号
        v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
        params.push_back(CParam("工艺参数序号", "", this->getName().c_str(), v));
        i += 2;
        if (v == 1) {
            // 2.A_腔加热时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("A_腔加热时间", "", this->getName().c_str(), v * 0.1f));
            i += 2;
            // 3.A_腔破真空时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("A_腔破真空时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
            // 4.A_腔真空到达值
            params.push_back(CParam("A_腔真空到达值", "", this->getName().c_str(), (double)toFloat(&pszData[i])));
            i += 4;
            // 5.A_腔温控表主控温度设定
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
            params.push_back(CParam("A_腔温控表主控温度设定", "", this->getName().c_str(), v * 0.1f));
            i += 4;
        }
        else if (v == 1) {
            // 2.B_腔加热时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("B_腔加热时间", "", this->getName().c_str(), v * 0.1f));
            i += 2;
            // 3.A_腔破真空时间
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8;
            params.push_back(CParam("B_腔破真空时间", "", this->getName().c_str(), v * 0.01f));
            i += 2;
            // 4.A_腔真空到达值
            params.push_back(CParam("B_腔真空到达值", "", this->getName().c_str(), (double)toFloat(&pszData[i])));
            i += 4;
            // 5.A_腔温控表主控温度设定
            v = (pszData[i] & 0xff) | (pszData[i + 1] & 0xff) << 8 | (pszData[i + 2] & 0xff) << 16 | (pszData[i + 3] & 0xff) << 24;
            params.push_back(CParam("B_腔温控表主控温度设定", "", this->getName().c_str(), v * 0.1f));
            i += 4;
        }
        return (int)params.size();
    }
    int CVacuumBake::parsingSVData(const char* pszData, size_t size, std::vector<CParam>& params)
SourceCode/Bond/Servo/ClientListDlg.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
#include "stdafx.h"
#include "ClientListDlg.h"
#include "afxdialogex.h"
#include "../DAQBridge/core/Collector.h"
#include "CMaster.h"
#include "Model.h"
#include "Servo.h"
#include <chrono>
#include <iomanip>
#include <sstream>
// CClientListDlg å¯¹è¯æ¡†
IMPLEMENT_DYNAMIC(CClientListDlg, CDialogEx)
CClientListDlg::CClientListDlg(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_DIALOG_CLIENT_LIST, pParent)
{
}
CClientListDlg::~CClientListDlg()
{
}
void CClientListDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_LIST_CLIENTS, m_listClients);
}
BEGIN_MESSAGE_MAP(CClientListDlg, CDialogEx)
    ON_BN_CLICKED(IDC_BUTTON_REFRESH, &CClientListDlg::OnBnClickedRefresh)
    ON_WM_SYSCOMMAND()
END_MESSAGE_MAP()
// CClientListDlg æ¶ˆæ¯å¤„理程序
BOOL CClientListDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    // åˆå§‹åŒ–列表控件
    m_listClients.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_HEADERDRAGDROP);
    // æ·»åŠ åˆ—
    m_listClients.InsertColumn(0, _T("IP地址"), LVCFMT_LEFT, 120);
    m_listClients.InsertColumn(1, _T("端口"), LVCFMT_CENTER, 80);
    m_listClients.InsertColumn(2, _T("版本状态"), LVCFMT_CENTER, 100);
    m_listClients.InsertColumn(3, _T("连接状态"), LVCFMT_CENTER, 100);
    m_listClients.InsertColumn(4, _T("连接时间"), LVCFMT_LEFT, 150);
    // åˆ·æ–°å®¢æˆ·ç«¯åˆ—表
    RefreshClientList();
    return TRUE;  // é™¤éžå°†ç„¦ç‚¹è®¾ç½®åˆ°æŽ§ä»¶ï¼Œå¦åˆ™è¿”回 TRUE
}
void CClientListDlg::OnBnClickedRefresh()
{
    RefreshClientList();
}
void CClientListDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    if (nID == SC_CLOSE)
    {
        CDialogEx::OnCancel();
        return;
    }
    CDialogEx::OnSysCommand(nID, lParam);
}
void CClientListDlg::RefreshClientList()
{
    // æ¸…空当前列表
    m_listClients.DeleteAllItems();
    m_clients.clear();
    // èŽ·å–Collector实例
    extern CServoApp theApp;
    SERVO::CMaster& master = theApp.m_model.getMaster();
    Collector* pCollector = master.getCollector();
    if (pCollector)
    {
        // èŽ·å–çœŸå®žçš„å®¢æˆ·ç«¯åˆ—è¡¨
        auto clientSummaries = pCollector->getClientList();
        // è½¬æ¢æ•°æ®æ ¼å¼
        for (const auto& summary : clientSummaries)
        {
            ClientInfo client;
            client.ip = summary.ip;
            client.port = summary.port;
            client.versionOk = summary.versionOk;
            client.status = summary.versionOk ? "已连接" : "版本不匹配";
            // èŽ·å–å½“å‰æ—¶é—´ä½œä¸ºè¿žæŽ¥æ—¶é—´ï¼ˆå®žé™…å®žçŽ°ä¸­åº”è¯¥ä»ŽCollector获取真实连接时间)
            auto now = std::chrono::system_clock::now();
            auto time_t = std::chrono::system_clock::to_time_t(now);
            std::tm tm;
            localtime_s(&tm, &time_t);
            std::ostringstream oss;
            oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
            client.connectTime = oss.str();
            m_clients.push_back(client);
        }
    }
    else
    {
        // å¦‚果无法获取Collector实例,显示提示信息
        ClientInfo noData;
        noData.ip = "无法获取数据";
        noData.port = 0;
        noData.versionOk = false;
        noData.status = "Collector未初始化";
        noData.connectTime = "";
        m_clients.push_back(noData);
    }
    // æ›´æ–°åˆ—表显示
    UpdateClientList(m_clients);
}
void CClientListDlg::UpdateClientList(const std::vector<ClientInfo>& clients)
{
    m_listClients.DeleteAllItems();
    for (size_t i = 0; i < clients.size(); ++i)
    {
        const ClientInfo& client = clients[i];
        int nItem = m_listClients.InsertItem(i, CString(client.ip.c_str()));
        m_listClients.SetItemText(nItem, 1, CString(std::to_string(client.port).c_str()));
        m_listClients.SetItemText(nItem, 2, client.versionOk ? _T("正常") : _T("异常"));
        m_listClients.SetItemText(nItem, 3, CString(client.status.c_str()));
        m_listClients.SetItemText(nItem, 4, CString(client.connectTime.c_str()));
    }
}
SourceCode/Bond/Servo/ClientListDlg.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
#pragma once
#include "afxwin.h"
#include <vector>
#include <string>
// å®¢æˆ·ç«¯ä¿¡æ¯ç»“æž„
struct ClientInfo {
    std::string ip;
    uint16_t port;
    bool versionOk;
    std::string status;
    std::string connectTime;
};
// CClientListDlg å¯¹è¯æ¡†
class CClientListDlg : public CDialogEx
{
    DECLARE_DYNAMIC(CClientListDlg)
public:
    CClientListDlg(CWnd* pParent = nullptr);   // æ ‡å‡†æž„造函数
    virtual ~CClientListDlg();
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_DIALOG_CLIENT_LIST };
#endif
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    DECLARE_MESSAGE_MAP()
public:
    virtual BOOL OnInitDialog();
    afx_msg void OnBnClickedRefresh();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
private:
    void RefreshClientList();
    void UpdateClientList(const std::vector<ClientInfo>& clients);
private:
    CListCtrl m_listClients;
    std::vector<ClientInfo> m_clients;
};
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/GlassJson.cpp
@@ -190,7 +190,38 @@
        root["path"] = std::move(arr);
    }
    root["payload_version"] = 1;
    // SV数据:三层结构序列化
    {
        Json::Value svDataObj(Json::objectValue);
        const auto& allSVData = g.getAllSVData();
        for (const auto& machinePair : allSVData) {
            int machineId = machinePair.first;
            const auto& dataTypes = machinePair.second;
            Json::Value machineObj(Json::objectValue);
            for (const auto& dataTypePair : dataTypes) {
                const std::string& dataType = dataTypePair.first;
                const auto& dataItems = dataTypePair.second;
                Json::Value dataArray(Json::arrayValue);
                for (const auto& item : dataItems) {
                    Json::Value itemObj(Json::objectValue);
                    // æ—¶é—´æˆ³è½¬æ¢ä¸ºæ¯«ç§’字符串
                    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
                        item.timestamp.time_since_epoch()).count();
                    itemObj["t"] = std::to_string(ms);
                    itemObj["v"] = item.value;
                    dataArray.append(itemObj);
                }
                machineObj[dataType] = dataArray;
            }
            svDataObj[std::to_string(machineId)] = machineObj;
        }
        root["sv_datas"] = svDataObj;
    }
    root["payload_version"] = 2; // ç‰ˆæœ¬å‡çº§ï¼Œå› ä¸ºæ–°å¢žäº†sv_datas字段
    return root;
}
@@ -308,6 +339,36 @@
            if (JBool(n, "processed", false)) tail->processEnd();
        }
    }
    // SV数据:反序列化三层结构
    if (root.isMember("sv_datas") && root["sv_datas"].isObject()) {
        g.clearAllSVData(); // æ¸…空现有数据
        const auto& svDataObj = root["sv_datas"];
        auto memberNames = svDataObj.getMemberNames();
        for (const auto& machineIdStr : memberNames) {
            int machineId = std::stoi(machineIdStr);
            const auto& machineObj = svDataObj[machineIdStr];
            auto dataTypeNames = machineObj.getMemberNames();
            for (const auto& dataType : dataTypeNames) {
                const auto& dataArray = machineObj[dataType];
                if (dataArray.isArray()) {
                    for (const auto& itemObj : dataArray) {
                        long long timestampMs = 0;
                        double value = 0.0;
                        if (get_ll_from_json(itemObj, "t", timestampMs)) {
                            value = JDouble(itemObj, "v", 0.0);
                            // æ·»åŠ SV数据
                            g.addSVData(machineId, dataType, timestampMs, value);
                        }
                    }
                }
            }
        }
    }
}
// ==================== ä¾¿æ·å°è£… ====================
@@ -333,4 +394,4 @@
    }
    FromJson(j, g);
    return true;
}
}
SourceCode/Bond/Servo/GlassLogDb.cpp
@@ -524,3 +524,55 @@
    ofs.flush();
    return rows;
}
std::optional<GlassLogDb::Row> GlassLogDb::queryById(long long id) {
    std::lock_guard<std::mutex> lk(mtx_);
    const char* sql =
        "SELECT id, cassette_seq_no, job_seq_no, class_id, material_type, state,"
        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_start, 'localtime'), ''),"
        " IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_end,   'localtime'), ''),"
        " buddy_id, aoi_result, path, params, pretty"
        " FROM " GLASS_LOG_TABLE
        " WHERE id = ?";
    sqlite3_stmt* stmt = nullptr;
    try {
        throwIf(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), db_, "prepare queryById");
        throwIf(sqlite3_bind_int64(stmt, 1, id), db_, "bind id");
        std::optional<Row> result;
        int rc = sqlite3_step(stmt);
        if (rc == SQLITE_ROW) {
            Row row;
            row.id = sqlite3_column_int64(stmt, 0);
            row.cassetteSeqNo = sqlite3_column_int(stmt, 1);
            row.jobSeqNo = sqlite3_column_int(stmt, 2);
            row.classId = safe_text(stmt, 3);
            row.materialType = sqlite3_column_int(stmt, 4);
            row.state = sqlite3_column_int(stmt, 5);
            row.tStart = safe_text(stmt, 6);
            row.tEnd = safe_text(stmt, 7);
            row.buddyId = safe_text(stmt, 8);
            row.aoiResult = sqlite3_column_int(stmt, 9);
            row.path = safe_text(stmt, 10);
            row.params = safe_text(stmt, 11);
            row.pretty = safe_text(stmt, 12);
            result = std::move(row);
        }
        else if (rc != SQLITE_DONE) {
            throwIf(rc, db_, "queryById step");
        }
        sqlite3_finalize(stmt);
        return result;
    }
    catch (const std::exception& e) {
        if (stmt) sqlite3_finalize(stmt);
        TRACE("GlassLogDb::queryById exception: %s\n", e.what());
        return std::nullopt;
    }
}
SourceCode/Bond/Servo/GlassLogDb.h
@@ -96,6 +96,9 @@
        int limit = 50,
        int offset = 0);
    // æŒ‰ ID æŸ¥è¯¢å•条记录(包含 pretty å­—段)
    std::optional<Row> queryById(long long id);
    // ç»Ÿè®¡ä¸Ž filters ç›¸åŒæ¡ä»¶çš„æ€»æ•°
    long long count(const Filters& filters = {});
SourceCode/Bond/Servo/GroupLabel.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
#include "stdafx.h"
#include "GroupLabel.h"
CGroupLabel::CGroupLabel()
{
    m_crBkgnd = RGB(255, 255, 255);
    m_crLine = RGB(188, 188, 188);
    m_crText = RGB(88, 88, 88);
    m_nPadding[PADDING_LEFT] = 5;
    m_nPadding[PADDING_TOP] = 5;
    m_nPadding[PADDING_RIGHT] = 5;
    m_nPadding[PADDING_BOTTOM] = 5;
    m_nTextAlign = TEXT_ALIGN_CENTER;
    m_bEnableResize = FALSE;
}
CGroupLabel::~CGroupLabel()
{
}
BEGIN_MESSAGE_MAP(CGroupLabel, CStatic)
    ON_WM_PAINT()
    ON_WM_LBUTTONDOWN()
    ON_WM_SETCURSOR()
END_MESSAGE_MAP()
void CGroupLabel::EnableResize()
{
    m_bEnableResize = TRUE;
    DWORD dwStyle = GetStyle();
    dwStyle |= SS_NOTIFY;
    SetWindowLong(GetSafeHwnd(), GWL_STYLE, dwStyle);
}
void CGroupLabel::DisableResize()
{
    m_bEnableResize = FALSE;
}
void CGroupLabel::OnPaint()
{
    CPaintDC dc(this); // device context for painting
                       // TODO: åœ¨æ­¤å¤„添加消息处理程序代码
                       // ä¸ä¸ºç»˜å›¾æ¶ˆæ¯è°ƒç”¨ CStatic::OnPaint()
    HDC hMemDC;
    HBITMAP hBitmap;
    RECT rcClient;
    CString strText;
    HFONT hFont;
    HBRUSH hBrushBK;
    ::GetClientRect(m_hWnd, &rcClient);
    hMemDC = ::CreateCompatibleDC(dc.m_hDC);
    hBitmap = ::CreateCompatibleBitmap(dc.m_hDC, rcClient.right - rcClient.left,
        rcClient.bottom - rcClient.top);
    ::SelectObject(hMemDC, hBitmap);
    ::SetBkMode(hMemDC, TRANSPARENT);
    // èƒŒæ™¯é¢œè‰²
    hBrushBK = CreateSolidBrush(m_crBkgnd);
    ::FillRect(hMemDC, &rcClient, hBrushBK);
    DeleteObject(hBrushBK);
    // çª—口标题
    int x1, x2, x3, x4;
    x1 = rcClient.left + m_nPadding[PADDING_LEFT];
    x4 = rcClient.right - m_nPadding[PADDING_RIGHT];
    hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
    ::SelectObject(hMemDC, hFont);
    ::SetTextColor(hMemDC, m_crText);
    char szTitle[256];
    int nTextLen = ::GetWindowText(m_hWnd, szTitle, 256);
    if (nTextLen > 0) {
        RECT rcTitle;
        SIZE sizeText;
        rcTitle.left = x1;
        rcTitle.top = rcClient.top + 2;
        rcTitle.bottom = rcClient.bottom - 2;
        rcTitle.right = x4;
#define TEXT_SPAGE    12
        GetTextExtentPoint32(hMemDC, szTitle, nTextLen, &sizeText);
        if (TEXT_ALIGN_LEFT == m_nTextAlign) {
            ::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
            x1 += sizeText.cx + TEXT_SPAGE;
        }
        else if (TEXT_ALIGN_CENTER == m_nTextAlign) {
            ::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
            x2 = x1 + (x4 - x1 - sizeText.cx - TEXT_SPAGE * 2) / 2;
            x3 = x2 + sizeText.cx + TEXT_SPAGE * 2;
        }
        else {
            ::DrawText(hMemDC, szTitle, nTextLen, &rcTitle, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
            x4 -= (sizeText.cx + TEXT_SPAGE);
        }
    }
    // åˆ†éš”线
    HPEN hPen = CreatePen(PS_SOLID, 1, m_crLine);
    HPEN hOldPen = (HPEN)SelectObject(hMemDC, hPen);
    int y = (rcClient.bottom - rcClient.top) / 2;
    if (TEXT_ALIGN_LEFT == m_nTextAlign) {
        ::MoveToEx(hMemDC, x1, y, NULL);
        ::LineTo(hMemDC, x4, y);
    }
    else if (TEXT_ALIGN_CENTER == m_nTextAlign) {
        ::MoveToEx(hMemDC, x1, y, NULL);
        ::LineTo(hMemDC, x2, y);
        ::MoveToEx(hMemDC, x3, y, NULL);
        ::LineTo(hMemDC, x4, y);
    }
    else {
        ::MoveToEx(hMemDC, x1, y, NULL);
        ::LineTo(hMemDC, x4, y);
    }
    SelectObject(hMemDC, hOldPen);
    ::DeleteObject(hPen);
    // EndPaint
    ::BitBlt(dc.m_hDC, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top,
        hMemDC, 0, 0, SRCCOPY);
    ::DeleteObject(hBitmap);
    ::DeleteDC(hMemDC);
}
void CGroupLabel::SetTextAlignMode(int nAlign)
{
    if (nAlign == TEXT_ALIGN_LEFT || nAlign == TEXT_ALIGN_CENTER || nAlign == TEXT_ALIGN_RIGHT) {
        m_nTextAlign = nAlign;
        RECT rcClient;
        ::GetClientRect(m_hWnd, &rcClient);
        ::InvalidateRect(m_hWnd, &rcClient, TRUE);
    }
}
void CGroupLabel::Setpadding(int type, unsigned int nPadding)
{
    if (type >= PADDING_LEFT && PADDING_LEFT <= PADDING_BOTTOM) {
        m_nPadding[type] = nPadding;
    }
}
/*
 * è®¾ç½®èƒŒæ™¯è‰²
 * color -- èƒŒæ™¯è‰²
 */
void CGroupLabel::SetBkgndColor(COLORREF color)
{
    m_crBkgnd = color;
}
/*
 * è®¾ç½®æ–‡æœ¬è‰²
 * color -- èƒŒæ™¯è‰²
 */
void CGroupLabel::SetTextColor(COLORREF color)
{
    m_crText = color;
    RECT rcClient;
    ::GetClientRect(m_hWnd, &rcClient);
    ::InvalidateRect(m_hWnd, &rcClient, TRUE);
}
/*
 * è®¾ç½®çº¿è‰²
 * color -- èƒŒæ™¯è‰²
 */
void CGroupLabel::SetLineColor(COLORREF color)
{
    m_crLine = color;
    RECT rcClient;
    ::GetClientRect(m_hWnd, &rcClient);
    ::InvalidateRect(m_hWnd, &rcClient, TRUE);
}
 void CGroupLabel::OnLButtonDown(UINT nFlags, CPoint point)
 {
     if (!m_bEnableResize) {
         CStatic::OnLButtonDown(nFlags, point);
         return;
     }
     CPoint pt, ptNew;
     pt = point;
     int nMoveY = 0;
     // æ•捉鼠标消息,检测是否拖动
     CRect rcParent, rcWindows;
     GetClientRect(&rcWindows);
     ::ClientToScreen(m_hWnd, (LPPOINT)&rcWindows);
     ::ClientToScreen(m_hWnd, (LPPOINT)&rcWindows.right);
     GetParent()->GetClientRect(&rcParent);
     ::ClientToScreen(GetParent()->m_hWnd, (LPPOINT)&rcParent);
     HDC hDC = ::GetDC(::GetDesktopWindow());
     ::DrawFocusRect(hDC, &rcWindows);
     if (::GetCapture() == NULL) {
         SetCapture();
         ASSERT(this == GetCapture());
         AfxLockTempMaps();
         for (;;) {
             MSG msg;
             VERIFY(::GetMessage(&msg, NULL, 0, 0));
             if (GetCapture() != this) break;
             switch (msg.message)
             {
             case WM_MOUSEMOVE:
                 ptNew = msg.pt;
                 if (ptNew.y < rcParent.top) ptNew.y = rcParent.top;
                 ::DrawFocusRect(hDC, &rcWindows);
                 rcWindows.top = ptNew.y - 3;
                 rcWindows.bottom = ptNew.y + 3;
                 ::DrawFocusRect(hDC, &rcWindows);
                 ::ScreenToClient(m_hWnd, &ptNew);
                 break;
             case WM_LBUTTONUP:
                 ptNew = msg.pt;
                 ::ScreenToClient(m_hWnd, &ptNew);
                 nMoveY = ptNew.y - pt.y;
                 goto ExitLoop;
             case WM_KEYDOWN:
                 if (msg.wParam == VK_ESCAPE) {
                     goto ExitLoop;
                 }
                 break;
             default:
                 DispatchMessage(&msg);
                 break;
             }
         }
     ExitLoop:
         ::DrawFocusRect(hDC, &rcWindows);
         ::ReleaseDC(::GetDesktopWindow(), hDC);
         ReleaseCapture();
         ::InvalidateRect(m_hWnd, NULL, TRUE);
         GetParent()->SendMessage(ID_MSG_RESIZEY, nMoveY, 0);
         AfxUnlockTempMaps(FALSE);
     }
     CStatic::OnLButtonDown(nFlags, point);
 }
 BOOL CGroupLabel::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
 {
     if (m_bEnableResize) {
         ::SetCursor(::LoadCursor(NULL, IDC_SIZENS));
         return TRUE;
     }
     return CStatic::OnSetCursor(pWnd, nHitTest, message);
 }
SourceCode/Bond/Servo/GroupLabel.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
#pragma once
#define PADDING_LEFT        0
#define PADDING_TOP            1
#define PADDING_RIGHT        2
#define PADDING_BOTTOM        3
#define TEXT_ALIGN_LEFT        0
#define TEXT_ALIGN_CENTER    1
#define TEXT_ALIGN_RIGHT    3
#define ID_MSG_RESIZEY        WM_USER+123
class CGroupLabel : public CStatic
{
public:
    CGroupLabel();
    ~CGroupLabel();
public:
    void SetBkgndColor(COLORREF color);
    void SetTextColor(COLORREF color);
    void SetLineColor(COLORREF color);
    void Setpadding(int type, unsigned int nPadding);
    void SetTextAlignMode(int nAlign);
    void EnableResize();
    void DisableResize();
private:
    COLORREF    m_crBkgnd;
    unsigned int m_nPadding[4];
    COLORREF    m_crLine;
    COLORREF    m_crText;
    int            m_nTextAlign;
private:
    BOOL m_bEnableResize;
    DECLARE_MESSAGE_MAP()
    afx_msg void OnPaint();
public:
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
};
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/PageLog.cpp
@@ -84,7 +84,7 @@
                                        std::regex((LPTSTR)(LPCTSTR)m_strFilterText));
                                }
                                catch (const std::regex_error& e) {
                                    TRACE(_T("正在表达式匹配检测异常: %s\n"), e.what());
                                }
                            }
                            if (m_filterMode == FilterMode::Exclude) {
SourceCode/Bond/Servo/ProcessJob.cpp
@@ -7,9 +7,12 @@
namespace SERVO {
    static inline std::string trimCopy(std::string s) {
        auto notspace = [](int ch) { return !std::isspace(ch); };
        s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
        s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
        s.erase(s.begin(),
            std::find_if(s.begin(), s.end(),
                [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }));
        s.erase(std::find_if(s.rbegin(), s.rend(),
            [](char c) { return !std::isspace(static_cast<unsigned char>(c)); }).base(),
            s.end());
        return s;
    }
@@ -21,6 +24,12 @@
    CProcessJob::CProcessJob(std::string pjId)
        : m_pjId(trimCopy(pjId))
    {
        clampString(m_pjId, MAX_ID_LEN);
    }
    void CProcessJob::setId(std::string& id)
    {
        m_pjId = trimCopy(id);
        clampString(m_pjId, MAX_ID_LEN);
    }
@@ -61,7 +70,7 @@
        m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
    }
    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issues()
    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issues() const
    {
        return m_issues;
    }
@@ -177,9 +186,11 @@
        return true;
    }
    bool CProcessJob::abort() {
    bool CProcessJob::abort(std::string reason) {
        if (m_state == PJState::Completed || m_state == PJState::Aborted || m_state == PJState::Failed)
            return false;
        m_failReason = trimCopy(reason);
        clampString(m_failReason, 128);
        m_state = PJState::Aborted;
        markEnd();
        return true;
@@ -266,6 +277,31 @@
        }
    }
    bool CProcessJob::setCarrierSlotsAndContexts(std::string carrierId, std::vector<uint8_t> slots, std::vector<void*> contexts)
    {
        for (auto& c : m_carriers) {
            if (c.carrierId.compare(carrierId) == 0) {
                c.slots = std::move(slots);
                c.contexts = std::move(contexts);
                return true;
            }
        }
        return false;
    }
    bool CProcessJob::setCarrierContexts(std::string carrierId, std::vector<void*> contexts)
    {
        for (auto& c : m_carriers) {
            if (c.carrierId.compare(carrierId) == 0) {
                c.contexts = std::move(contexts);
                return true;
            }
        }
        return false;
    }
    // --------- æ ¸å¿ƒï¼šserialize/deserialize ---------
    void CProcessJob::serialize(std::ostream& os) const {
        // Í·
@@ -278,7 +314,7 @@
        // é…æ–¹
        uint8_t recipeType = static_cast<uint8_t>(m_recipeMethod);
        write_pod(os, m_recipeMethod);
        write_pod(os, recipeType);
        write_string(os, m_recipeSpec);
        // ç‰©æ–™ï¼ˆå¤š Carrier & Slot)
@@ -399,13 +435,13 @@
            return "InProcess";
            break;
        case SERVO::PJState::Paused:
            return "Queued";
            return "Paused";
            break;
        case SERVO::PJState::Aborting:
            return "Aborting";
            break;
        case SERVO::PJState::Completed:
            return "Queued";
            return "Completed";
            break;
        case SERVO::PJState::Aborted:
            return "Aborted";
@@ -420,7 +456,7 @@
        return "";
    }
    CarrierSlotInfo* CProcessJob::getCarrier(std::string& strId)
    CarrierSlotInfo* CProcessJob::getCarrier(const  std::string& strId)
    {
        for (auto& item : m_carriers) {
            if (item.carrierId.compare(strId) == 0) {
@@ -430,4 +466,44 @@
        return nullptr;
    }
    void CProcessJob::setLotId(std::string strLotId)
    {
        m_strLotId = strLotId;
    }
    std::string& CProcessJob::getLotId()
    {
        return m_strLotId;
    }
    void CProcessJob::setProductId(std::string strProductId)
    {
        m_strProductId = strProductId;
    }
    std::string& CProcessJob::getProductId()
    {
        return m_strProductId;
    }
    void CProcessJob::setOperationId(std::string strOperationId)
    {
        m_strOperationId = strOperationId;
    }
    std::string& CProcessJob::getOperationId()
    {
        return m_strOperationId;
    }
    void CProcessJob::setPjWarp(PJWarp pjWarp)
    {
        m_pjWarp = pjWarp;
    }
    PJWarp& CProcessJob::getPjWarp()
    {
        return m_pjWarp;
    }
}
SourceCode/Bond/Servo/ProcessJob.h
@@ -8,6 +8,16 @@
#include <chrono>
#include <optional>
struct PJWarp {
    BOOL addToCj;
    void* pj;
    int port;
    BOOL checkSlot[8];
    int material[8];
};
namespace SERVO {
    /// PJ ç”Ÿå‘½å‘¨æœŸï¼ˆè´´è¿‘ E40 å¸¸è§çŠ¶æ€ï¼‰
    enum class PJState : uint8_t {
@@ -90,6 +100,7 @@
        explicit CProcessJob(std::string pjId);
        const std::string& id() const noexcept { return m_pjId; }
        void setId(std::string& id);
        const std::string& parentCjId() const noexcept { return m_parentCjId; }
        PJState state() const noexcept { return m_state; }
        StartPolicy startPolicy() const noexcept { return m_startPolicy; }
@@ -121,7 +132,7 @@
        };
        // è¿”回问题清单(空=通过)
        bool validate(const IResourceView& rv);
        const std::vector<ValidationIssue>& issues();
        const std::vector<ValidationIssue>& issues() const;
        // â€”— çŠ¶æ€æœºï¼ˆå¸¦å®ˆå«ï¼‰â€”â€”
        bool queue();           // NoState -> Queued
@@ -130,7 +141,7 @@
        bool pause();           // InProcess -> Paused
        bool resume();          // Paused -> InProcess
        bool complete();        // InProcess -> Completed
        bool abort();           // Any (未终态) -> Aborted
        bool abort(std::string reason);           // Any (未终态) -> Aborted
        bool fail(std::string reason); // ä»»æ„æ€ -> Failed(记录失败原因)
        // â€”— è®¿é—®å™¨ï¼ˆç”¨äºŽä¸ŠæŠ¥/查询)——
@@ -153,9 +164,13 @@
        // è¿½åŠ ä¸€ä¸ªè½½å…·
        void addCarrier(std::string carrierId, std::vector<uint8_t> slots);
        // è®¾ç½®è½½å…·slots和contexts
        bool setCarrierSlotsAndContexts(std::string carrierId, std::vector<uint8_t> slots, std::vector<void*> contexts);
        bool setCarrierContexts(std::string carrierId, std::vector<void*> contexts);
        // è®¿é—®å™¨
        const std::vector<CarrierSlotInfo>& carriers() const noexcept { return m_carriers; }
        CarrierSlotInfo* getCarrier(std::string& strId);
        CarrierSlotInfo* getCarrier(const std::string& strId);
        // åˆ¤å®šæ˜¯å¦â€œæŒ‰è½½å…·/卡位”方式
        bool usesCarrierSlots() const noexcept { return !m_carriers.empty(); }
@@ -210,6 +225,22 @@
        // é”™è¯¯åˆ—表
        std::vector<ValidationIssue> m_issues;
        // æ–°å¢ž
        std::string m_strLotId;
        std::string m_strProductId;
        std::string m_strOperationId;
        PJWarp m_pjWarp;
    public:
        void setLotId(std::string strLotId);
        std::string& getLotId();
        void setProductId(std::string strProductId);
        std::string& getProductId();
        void setOperationId(std::string strOperationId);
        std::string& getOperationId();
        void setPjWarp(PJWarp pjWarp);
        PJWarp& getPjWarp();
    };
}
SourceCode/Bond/Servo/Servo.cpp
@@ -16,6 +16,7 @@
#include "EqsGraphWnd.h"
#include "MapPosWnd.h"
#include "HmTab.h"
#include "CControlJobManagerDlg.h"
// å£°æ˜Žå…¨å±€å˜é‡ï¼Œç”¨äºŽç®¡ç† GDI+ åˆå§‹åŒ–
@@ -226,6 +227,7 @@
int CServoApp::ExitInstance()
{
    CControlJobManagerDlg::FreeState();
    m_model.term();
    HSMS_Term();
    RX_Term();
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" />
@@ -210,14 +223,21 @@
    <ClInclude Include="..\jsoncpp\include\json\writer.h" />
    <ClInclude Include="..\jsoncpp\lib_json\json_batchallocator.h" />
    <ClInclude Include="CBaseDlg.h" />
    <ClInclude Include="CCarrierSlotGrid.h" />
    <ClInclude Include="CCarrierSlotSelector.h" />
    <ClInclude Include="CCjPage2.h" />
    <ClInclude Include="CCjPage3.h" />
    <ClInclude Include="CCjPageBase.h" />
    <ClInclude Include="CControlJob.h" />
    <ClInclude Include="CControlJobDlg.h" />
    <ClInclude Include="CControlJobManagerDlg.h" />
    <ClInclude Include="CCustomCheckBox.h" />
    <ClInclude Include="CCollectionEvent.h" />
    <ClInclude Include="CEquipmentPage3.h" />
    <ClInclude Include="CExpandableListCtrl.h" />
    <ClInclude Include="CGlassPool.h" />
    <ClInclude Include="ChangePasswordDlg.h" />
    <ClInclude Include="ClientListDlg.h" />
    <ClInclude Include="CMyStatusbar.h" />
    <ClInclude Include="CPageCollectionEvent.h" />
    <ClInclude Include="CPageGlassList.h" />
@@ -225,6 +245,7 @@
    <ClInclude Include="CPageReport.h" />
    <ClInclude Include="CPageVarialbles.h" />
    <ClInclude Include="CParam.h" />
    <ClInclude Include="CCjPage1.h" />
    <ClInclude Include="CProcessDataListDlg.h" />
    <ClInclude Include="CReport.h" />
    <ClInclude Include="CRobotCmdContainerDlg.h" />
@@ -254,6 +275,7 @@
    <ClInclude Include="GridControl\TitleTip.h" />
    <ClInclude Include="CRobotTask.h" />
    <ClInclude Include="CSlot.h" />
    <ClInclude Include="GroupLabel.h" />
    <ClInclude Include="HorizontalLine.h" />
    <ClInclude Include="InputDialog.h" />
    <ClInclude Include="JobSlotGrid.h" />
@@ -373,6 +395,34 @@
    <ClInclude Include="VerticalLine.h" />
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="..\DAQBridge\buffer\BufferManager.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\buffer\BufferRegistry.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\buffer\SampleBuffer.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\core\Collector.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\core\Display.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\net\SocketComm.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\proto\ProtocolCodec.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="..\jsoncpp\lib_json\json_reader.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -386,14 +436,21 @@
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
    </ClCompile>
    <ClCompile Include="CBaseDlg.cpp" />
    <ClCompile Include="CCarrierSlotGrid.cpp" />
    <ClCompile Include="CCarrierSlotSelector.cpp" />
    <ClCompile Include="CCjPage2.cpp" />
    <ClCompile Include="CCjPage3.cpp" />
    <ClCompile Include="CCjPageBase.cpp" />
    <ClCompile Include="CControlJob.cpp" />
    <ClCompile Include="CControlJobDlg.cpp" />
    <ClCompile Include="CControlJobManagerDlg.cpp" />
    <ClCompile Include="CCustomCheckBox.cpp" />
    <ClCompile Include="CCollectionEvent.cpp" />
    <ClCompile Include="CEquipmentPage3.cpp" />
    <ClCompile Include="CExpandableListCtrl.cpp" />
    <ClCompile Include="CGlassPool.cpp" />
    <ClCompile Include="ChangePasswordDlg.cpp" />
    <ClCompile Include="ClientListDlg.cpp" />
    <ClCompile Include="CMyStatusbar.cpp" />
    <ClCompile Include="CPageCollectionEvent.cpp" />
    <ClCompile Include="CPageGlassList.cpp" />
@@ -401,6 +458,7 @@
    <ClCompile Include="CPageReport.cpp" />
    <ClCompile Include="CPageVarialbles.cpp" />
    <ClCompile Include="CParam.cpp" />
    <ClCompile Include="CCjPage1.cpp" />
    <ClCompile Include="CProcessDataListDlg.cpp" />
    <ClCompile Include="CReport.cpp" />
    <ClCompile Include="CRobotCmdContainerDlg.cpp" />
@@ -428,6 +486,7 @@
    <ClCompile Include="GridControl\TitleTip.cpp" />
    <ClCompile Include="CRobotTask.cpp" />
    <ClCompile Include="CSlot.cpp" />
    <ClCompile Include="GroupLabel.cpp" />
    <ClCompile Include="HorizontalLine.cpp" />
    <ClCompile Include="InputDialog.cpp" />
    <ClCompile Include="JobSlotGrid.cpp" />
SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -198,6 +198,35 @@
    <ClCompile Include="GlassLogDb.cpp" />
    <ClCompile Include="sqlite3.c" />
    <ClCompile Include="CProcessDataListDlg.cpp" />
    <ClCompile Include="GroupLabel.cpp" />
    <ClCompile Include="CControlJobManagerDlg.cpp" />
    <ClCompile Include="CCjPage1.cpp" />
    <ClCompile Include="CCjPage2.cpp" />
    <ClCompile Include="CCjPage3.cpp" />
    <ClCompile Include="CCjPageBase.cpp" />
    <ClCompile Include="CCarrierSlotSelector.cpp" />
    <ClCompile Include="CCarrierSlotGrid.cpp" />
    <ClCompile Include="..\DAQBridge\buffer\BufferManager.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\buffer\BufferRegistry.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\buffer\SampleBuffer.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\core\Collector.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\core\Display.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\net\SocketComm.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
    <ClCompile Include="..\DAQBridge\proto\ProtocolCodec.cpp">
      <Filter>DAQBridge</Filter>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="AlarmManager.h" />
@@ -421,6 +450,53 @@
    <ClInclude Include="sqlite3.h" />
    <ClInclude Include="sqlite3ext.h" />
    <ClInclude Include="CProcessDataListDlg.h" />
    <ClInclude Include="GroupLabel.h" />
    <ClInclude Include="CControlJobManagerDlg.h" />
    <ClInclude Include="CCjPage1.h" />
    <ClInclude Include="CCjPage2.h" />
    <ClInclude Include="CCjPage3.h" />
    <ClInclude Include="CCjPageBase.h" />
    <ClInclude Include="CCarrierSlotSelector.h" />
    <ClInclude Include="CCarrierSlotGrid.h" />
    <ClInclude Include="..\DAQBridge\buffer\BufferManager.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\buffer\BufferRegistry.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\buffer\SampleBuffer.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\core\Collector.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\core\CommBase.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\core\ConnEvents.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\core\DataTypes.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\core\Display.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\net\FrameAssembler.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\net\SocketComm.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\proto\Protocol.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\proto\ProtocolCodec.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
    <ClInclude Include="..\DAQBridge\DAQConfig.h">
      <Filter>DAQBridge</Filter>
    </ClInclude>
  </ItemGroup>
  <ItemGroup>
    <ResourceCompile Include="Servo.rc" />
@@ -460,5 +536,8 @@
    <Filter Include="JsonCpp">
      <UniqueIdentifier>{a877af37-2f78-484f-b1bb-276edbbcd694}</UniqueIdentifier>
    </Filter>
    <Filter Include="DAQBridge">
      <UniqueIdentifier>{885738f6-3122-4bb9-8308-46b7f692fb13}</UniqueIdentifier>
    </Filter>
  </ItemGroup>
</Project>
SourceCode/Bond/Servo/Servo.vcxproj.user
@@ -7,6 +7,6 @@
    <RemoteDebuggerCommand>\\DESKTOP-IODBVIQ\Servo\Debug\Servo.exe</RemoteDebuggerCommand>
    <RemoteDebuggerWorkingDirectory>\\DESKTOP-IODBVIQ\Servo\Debug\</RemoteDebuggerWorkingDirectory>
    <RemoteDebuggerServerName>DESKTOP-IODBVIQ</RemoteDebuggerServerName>
    <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
    <DebuggerFlavor>WindowsRemoteDebugger</DebuggerFlavor>
  </PropertyGroup>
</Project>
SourceCode/Bond/Servo/ServoDlg.cpp
@@ -1,5 +1,5 @@
// ServoDlg.cpp : å®žçŽ°æ–‡ä»¶

// ServoDlg.cpp : å®žçŽ°æ–‡ä»¶
//
#include "stdafx.h"
@@ -28,6 +28,8 @@
#include "CPageCollectionEvent.h"
#include "CControlJobDlg.h"
#include "InputDialog.h"
#include "ClientListDlg.h"
#include "CControlJobManagerDlg.h"
#ifdef _DEBUG
@@ -35,32 +37,32 @@
#endif
/* åˆ›å»ºç»ˆç«¯çš„定时器 */
/* åˆ›å»ºç»ˆç«¯çš„定时器 */
#define TIMER_ID_CREATE_TERMINAL        1
/* è¿è¡Œæ—¶é—´å®šæ—¶å™¨ */
/* è¿è¡Œæ—¶é—´å®šæ—¶å™¨ */
#define TIMER_ID_UPDATE_RUMTIME            2
/* Test */
#define TIMER_ID_TEST                    3
// ç”¨äºŽåº”用程序“关于”菜单项的 CAboutDlg å¯¹è¯æ¡†
// ç”¨äºŽåº”用程序“关于”菜单项的 CAboutDlg å¯¹è¯æ¡†
class CAboutDlg : public CDialogEx
{
public:
    CAboutDlg();
// å¯¹è¯æ¡†æ•°æ®
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_ABOUTBOX };
#endif
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
// å®žçް
// å®žçް
protected:
    DECLARE_MESSAGE_MAP()
};
@@ -78,7 +80,7 @@
END_MESSAGE_MAP()
// CServoDlg å¯¹è¯æ¡†
// CServoDlg å¯¹è¯æ¡†
CServoDlg::CServoDlg(CWnd* pParent /*=NULL*/)
@@ -133,6 +135,8 @@
    ON_UPDATE_COMMAND_UI(ID_MENU_TEST_MESSAGE_SET, &CServoDlg::OnUpdateMenuTestMessageSet)
    ON_COMMAND(ID_MENU_TEST_MESSAGE_CLEAR, &CServoDlg::OnMenuTestMessageClear)
    ON_UPDATE_COMMAND_UI(ID_MENU_TEST_MESSAGE_CLEAR, &CServoDlg::OnUpdateMenuTestMessageClear)
    ON_COMMAND(ID_MENU_TOOLS_CLIENT_LIST, &CServoDlg::OnMenuToolsClientList)
    ON_UPDATE_COMMAND_UI(ID_MENU_TOOLS_CLIENT_LIST, &CServoDlg::OnUpdateMenuToolsClientList)
    ON_COMMAND(ID_MENU_HELP_ABOUT, &CServoDlg::OnMenuHelpAbout)
    ON_WM_INITMENUPOPUP()
    ON_WM_TIMER()
@@ -143,12 +147,12 @@
END_MESSAGE_MAP()
// CServoDlg æ¶ˆæ¯å¤„理程序
// CServoDlg æ¶ˆæ¯å¤„理程序
void CServoDlg::InitRxWindows()
{
    /* code */
    // è®¢é˜…数据
    // è®¢é˜…数据
    IRxWindows* pRxWindows = RX_GetRxWindows();
    pRxWindows->enableLog(5);
    if (m_pObserver == NULL) {
@@ -207,7 +211,7 @@
                else if (state == SERVO::MASTERSTATE::STARTING) {
                    m_pMyStatusbar->setBackgroundColor(STATUSBAR_BK_STARTING);
                    m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
                    m_pMyStatusbar->setRunTimeText("正在启动...");
                    m_pMyStatusbar->setRunTimeText("正在启动...");
                }
                else if (state == SERVO::MASTERSTATE::MSERROR) {
                    m_pTopToolbar->GetBtn(IDC_BUTTON_RUN)->EnableWindow(TRUE);
@@ -216,7 +220,7 @@
                    m_pTopToolbar->GetBtn(IDC_BUTTON_STOP)->EnableWindow(FALSE);
                    m_pMyStatusbar->setBackgroundColor(STATUSBAR_BK_ALARM);
                    m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
                    m_pMyStatusbar->setRunTimeText("启动失败.");
                    m_pMyStatusbar->setRunTimeText("启动失败.");
                    m_pTopToolbar->GetBtn(IDC_BUTTON_ALARM)->EnableWindow(TRUE);
                    KillTimer(TIMER_ID_UPDATE_RUMTIME);
                }
@@ -229,7 +233,7 @@
                    m_pMyStatusbar->setForegroundColor(RGB(0, 0, 0));
                    m_pMyStatusbar->setRunTimeText(theApp.m_model.getMaster().getLastErrorText().c_str());
                    if (theApp.m_model.getMaster().getLastError() == ER_CODE_AOI_NG) {
                        AfxMessageBox(_T("AOI检测失败,请操作员介入解决问题!"));
                        AfxMessageBox(_T("AOI检测失败,请操作员介入解决问题!"));
                    }
                    m_pTopToolbar->GetBtn(IDC_BUTTON_ALARM)->EnableWindow(TRUE);
                    KillTimer(TIMER_ID_UPDATE_RUMTIME);
@@ -273,13 +277,13 @@
                        }
                    }
                    else if (exCode == ROBOT_EVENT_FINISH) {
                        m_pMyStatusbar->setCurTaskBtnText("无");
                        m_pMyStatusbar->setCurTaskBtnText("无");
                    }
                    else if (exCode == ROBOT_EVENT_ABORT) {
                        m_pMyStatusbar->setCurTaskBtnText("无");
                        m_pMyStatusbar->setCurTaskBtnText("无");
                    }
                    else if (exCode == ROBOT_EVENT_RESTORE) {
                        m_pMyStatusbar->setCurTaskBtnText("无");
                        m_pMyStatusbar->setCurTaskBtnText("无");
                    }
                }
            }
@@ -329,9 +333,9 @@
{
    CDialogEx::OnInitDialog();
    // å°†â€œå…³äºŽ...”菜单项添加到系统菜单中。
    // å°†â€œå…³äºŽ...”菜单项添加到系统菜单中。
    // IDM_ABOUTBOX å¿…须在系统命令范围内。
    // IDM_ABOUTBOX å¿…须在系统命令范围内。
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);
@@ -349,17 +353,17 @@
        }
    }
    // è®¾ç½®æ­¤å¯¹è¯æ¡†çš„图标。  å½“应用程序主窗口不是对话框时,框架将自动
    //  æ‰§è¡Œæ­¤æ“ä½œ
    SetIcon(m_hIcon, TRUE);            // è®¾ç½®å¤§å›¾æ ‡
    SetIcon(m_hIcon, FALSE);        // è®¾ç½®å°å›¾æ ‡
    // è®¾ç½®æ­¤å¯¹è¯æ¡†çš„图标。  å½“应用程序主窗口不是对话框时,框架将自动
    //  æ‰§è¡Œæ­¤æ“ä½œ
    SetIcon(m_hIcon, TRUE);            // è®¾ç½®å¤§å›¾æ ‡
    SetIcon(m_hIcon, FALSE);        // è®¾ç½®å°å›¾æ ‡
    // model init
    theApp.m_model.init();
    SetTimer(TIMER_ID_TEST, 1000, nullptr);
    // èœå•
    // èœå•
    CMenu menu;
    menu.LoadMenu(IDR_MENU_APP);
    SetMenu(&menu);
@@ -396,13 +400,13 @@
    CHmTab* m_pTab = CHmTab::Hook(GetDlgItem(IDC_TAB1)->m_hWnd);
    m_pTab->SetPaddingLeft(20);
    m_pTab->SetItemMarginLeft(18);
    m_pTab->AddItem("״̬ͼ", FALSE);
    m_pTab->AddItem("连接图", TRUE);
    m_pTab->AddItem("状态图", FALSE);
    m_pTab->AddItem("连接图", TRUE);
    m_pTab->AddItem("Glass", TRUE);
    m_pTab->AddItem("配方", TRUE);
    m_pTab->AddItem("报警", TRUE);
    m_pTab->AddItem("日志", TRUE);
    m_pTab->AddItem("搬运任务", TRUE);
    m_pTab->AddItem("配方", TRUE);
    m_pTab->AddItem("报警", TRUE);
    m_pTab->AddItem("日志", TRUE);
    m_pTab->AddItem("搬运任务", TRUE);
    m_pTab->SetCurSel(0);
    m_pTab->SetBkgndColor(RGB(222, 222, 222));
    ShowChildPage(0);
@@ -424,7 +428,7 @@
    // è°ƒæ•´åˆå§‹çª—口位置
    // è°ƒæ•´åˆå§‹çª—口位置
    CRect rcWnd;
    GetWindowRect(&rcWnd);
    int width = GetSystemMetrics(SM_CXSCREEN);
@@ -438,22 +442,22 @@
    Resize();
    // ç›¸å½“于延时调用master的初始化
    // ç›¸å½“于延时调用master的初始化
    theApp.m_model.m_master.init();
    theApp.m_model.loadPortParams();
    // åˆå§‹åŒ–master以后需要控件绑定数据
    // åˆå§‹åŒ–master以后需要控件绑定数据
    m_pPageGraph1->BindEquipmentToGraph();
    // æ›´æ–°ç™»å½•状态
    // æ›´æ–°ç™»å½•状态
    UpdateLoginStatus();
    //SystemLogManager::getInstance.log(SystemLogManager::LogType::Info, _T("BondEq启动..."));
    //SystemLogManager::getInstance.log(SystemLogManager::LogType::Info, _T("BondEq启动..."));
    //SystemLogManager::getInstance.
    return TRUE;  // é™¤éžå°†ç„¦ç‚¹è®¾ç½®åˆ°æŽ§ä»¶ï¼Œå¦åˆ™è¿”回 TRUE
    return TRUE;  // é™¤éžå°†ç„¦ç‚¹è®¾ç½®åˆ°æŽ§ä»¶ï¼Œå¦åˆ™è¿”回 TRUE
}
void CServoDlg::OnSysCommand(UINT nID, LPARAM lParam)
@@ -469,19 +473,19 @@
    }
}
// å¦‚果向对话框添加最小化按钮,则需要下面的代码
//  æ¥ç»˜åˆ¶è¯¥å›¾æ ‡ã€‚  å¯¹äºŽä½¿ç”¨æ–‡æ¡£/视图模型的 MFC åº”用程序,
//  è¿™å°†ç”±æ¡†æž¶è‡ªåŠ¨å®Œæˆã€‚
// å¦‚果向对话框添加最小化按钮,则需要下面的代码
//  æ¥ç»˜åˆ¶è¯¥å›¾æ ‡ã€‚  å¯¹äºŽä½¿ç”¨æ–‡æ¡£/视图模型的 MFC åº”用程序,
//  è¿™å°†ç”±æ¡†æž¶è‡ªåŠ¨å®Œæˆã€‚
void CServoDlg::OnPaint()
{
    if (IsIconic())
    {
        CPaintDC dc(this); // ç”¨äºŽç»˜åˆ¶çš„设备上下文
        CPaintDC dc(this); // ç”¨äºŽç»˜åˆ¶çš„设备上下文
        SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
        // ä½¿å›¾æ ‡åœ¨å·¥ä½œåŒºçŸ©å½¢ä¸­å±…中
        // ä½¿å›¾æ ‡åœ¨å·¥ä½œåŒºçŸ©å½¢ä¸­å±…中
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
@@ -489,7 +493,7 @@
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;
        // ç»˜åˆ¶å›¾æ ‡
        // ç»˜åˆ¶å›¾æ ‡
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
@@ -498,8 +502,8 @@
    }
}
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CServoDlg::OnQueryDragIcon()
{
    return static_cast<HCURSOR>(m_hIcon);
@@ -659,6 +663,17 @@
    pCmdUI->Enable(TRUE);
}
void CServoDlg::OnMenuToolsClientList()
{
    CClientListDlg dlg;
    dlg.DoModal();
}
void CServoDlg::OnUpdateMenuToolsClientList(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(TRUE);
}
void CServoDlg::OnMenuHelpAbout()
{
    theApp.m_model.getMaster().test();
@@ -668,14 +683,14 @@
void CServoDlg::OnBnClickedOk()
{
    // TODO: åœ¨æ­¤æ·»åŠ æŽ§ä»¶é€šçŸ¥å¤„ç†ç¨‹åºä»£ç 
    // TODO: åœ¨æ­¤æ·»åŠ æŽ§ä»¶é€šçŸ¥å¤„ç†ç¨‹åºä»£ç 
    CDialogEx::OnOK();
}
void CServoDlg::OnBnClickedCancel()
{
    // TODO: åœ¨æ­¤æ·»åŠ æŽ§ä»¶é€šçŸ¥å¤„ç†ç¨‹åºä»£ç 
    // TODO: åœ¨æ­¤æ·»åŠ æŽ§ä»¶é€šçŸ¥å¤„ç†ç¨‹åºä»£ç 
    CDialogEx::OnCancel();
}
@@ -869,7 +884,7 @@
void CServoDlg::OnClose()
{
    // TODO: åœ¨æ­¤æ·»åŠ æ¶ˆæ¯å¤„ç†ç¨‹åºä»£ç å’Œ/或调用默认值
    // TODO: åœ¨æ­¤æ·»åŠ æ¶ˆæ¯å¤„ç†ç¨‹åºä»£ç å’Œ/或调用默认值
    CDialogEx::OnClose();
}
@@ -894,7 +909,7 @@
void CServoDlg::OnTimer(UINT_PTR nIDEvent)
{
    if (TIMER_ID_CREATE_TERMINAL == nIDEvent) {
        // é¢„先创建终端窗口
        // é¢„先创建终端窗口
        KillTimer(TIMER_ID_CREATE_TERMINAL);
        char szBuffer[MAX_PATH];
        sprintf_s(szBuffer, MAX_PATH, "%s\\RES\\TeminalMsg.html", (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
@@ -981,7 +996,7 @@
        ::EnableMenuItem(hMenu, ID_OPEATOR_SWITCH, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
        ::EnableMenuItem(hMenu, ID_OPERATOR_LOGOUT, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
        m_pTopToolbar->SetOperatorBtnText(_T("未登录"));
        m_pTopToolbar->SetOperatorBtnText(_T("未登录"));
    }
}
@@ -989,30 +1004,16 @@
{
    int id = (int)lParam;
    if (id == IDC_BUTTON_RUN || id == IDC_BUTTON_STOP) {
        //CInputDialog inputDialog(_T("验证用户"), _T("请输入用户密码:"));
        //if (inputDialog.DoModal() != IDOK) {
        //    AfxMessageBox(_T("取消验证!"));
        //    return 0;
        //}
        //CString inputText = inputDialog.GetInputText();
        //std::string strPass = UserManager::getInstance().getCurrentPass();
        //if (inputText.Compare(strPass.c_str()) != 0) {
        //    AfxMessageBox(_T("密码错误!"));
        //    SystemLogManager::getInstance().log(SystemLogManager::LogType::Info, _T("验证时,密码错误!"));
        //    return 0;
        //}
        UserRole emRole = UserManager::getInstance().getCurrentUserRole();
        if (emRole != UserRole::SuperAdmin) {
            AfxMessageBox(_T("当前用户并非管理员!!!"));
            AfxMessageBox(_T("当前用户并非管理员!!!"));
            return 1;
        }
    }
    if (id == IDC_BUTTON_RUN) {
        if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
        }
        else {
            if (theApp.m_model.getMaster().start() == 0) {
@@ -1024,7 +1025,7 @@
    }
    else if (id == IDC_BUTTON_RUN_BATCH) {
        if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
        }
        else {
            if (theApp.m_model.getMaster().startBatch() == 0) {
@@ -1036,7 +1037,7 @@
    }
    else if (id == IDC_BUTTON_RUN_CT) {
        if (theApp.m_model.getMaster().getState() == SERVO::MASTERSTATE::MSERROR) {
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
            AfxMessageBox("当前有机台发生错误,不能启动,请确认解决问题后再尝试重新启动!");
        }
        else {
            if (theApp.m_model.getMaster().startContinuousTransfer() == 0) {
@@ -1053,11 +1054,14 @@
    }
    else if (id == IDC_BUTTON_JOBS) {
        CControlJobDlg dlg;
        dlg.SetControlJob(theApp.m_model.m_master.getControlJob());
        dlg.DoModal();
    }
    else if (id == IDC_BUTTON_PORT_CONFIG) {
        CPortConfigurationDlg dlg;
        dlg.DoModal();
    }
    else if (id == IDC_BUTTON_CASSETTE) {
        CControlJobManagerDlg dlg;
        dlg.DoModal();
    }
    else if (id == IDC_BUTTON_ROBOT) {
@@ -1092,7 +1096,7 @@
        else if (2 == menuId) {
            CUserManagerDlg dlg;
            if (dlg.DoModal() != IDOK) {
                logManager.log(SystemLogManager::LogType::Operation, _T("用户管理的预操作被取消!"));
                logManager.log(SystemLogManager::LogType::Operation, _T("用户管理的预操作被取消!"));
            }
        }
        else if (3 == menuId) {
@@ -1100,14 +1104,14 @@
            dlg.DoModal();
        }
        else if (4 == menuId) {
            int ret = AfxMessageBox(_T("是否切换用户?切换用户会退出当前账号!"), MB_OKCANCEL | MB_ICONEXCLAMATION);
            int ret = AfxMessageBox(_T("是否切换用户?切换用户会退出当前账号!"), MB_OKCANCEL | MB_ICONEXCLAMATION);
            if (ret != IDOK) {
                return 0;
            }
            logManager.log(SystemLogManager::LogType::Operation, _T("确认切换角色!"));
            logManager.log(SystemLogManager::LogType::Operation, _T("确认切换角色!"));
            if (userManager.isLoggedIn()) {
                logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
                logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
                userManager.logout();
            }
@@ -1116,13 +1120,13 @@
        }
        else {
            CString cstrMessage;
            cstrMessage.Format(_T("是否退出用户 [%s]?"), userManager.getCurrentUser().c_str());
            cstrMessage.Format(_T("是否退出用户 [%s]?"), userManager.getCurrentUser().c_str());
            int ret = AfxMessageBox(_T(cstrMessage), MB_OKCANCEL | MB_ICONEXCLAMATION);
            if (ret != IDOK) {
                return 0;
            }
            logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
            logManager.log(SystemLogManager::LogType::Info, _T("退出登录!"));
            userManager.logout();
        }
@@ -1166,16 +1170,16 @@
    SERVO::MASTERSTATE state = theApp.m_model.getMaster().getState();
    if (state == SERVO::MASTERSTATE::RUNNING) {
        strText.Format(_T("正在运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
        strText.Format(_T("正在运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
    }
    else if (state == SERVO::MASTERSTATE::RUNNING_CONTINUOUS_TRANSFER) {
        strText.Format(_T("千传模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
        strText.Format(_T("千传模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
    }
    else if (state == SERVO::MASTERSTATE::RUNNING_BATCH) {
        strText.Format(_T("JOB模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
        strText.Format(_T("JOB模式:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
    }
    else {
        strText.Format(_T("已运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
        strText.Format(_T("已运行:%02d:%02d:%02d   %s"), h, m, s, pszSuffix);
    }
    return strText;
SourceCode/Bond/Servo/ServoDlg.h
@@ -1,5 +1,5 @@
// ServoDlg.h : å¤´æ–‡ä»¶

// ServoDlg.h : å¤´æ–‡ä»¶
//
#pragma once
@@ -20,12 +20,12 @@
#include "CPageGlassList.h"
// CServoDlg å¯¹è¯æ¡†
// CServoDlg å¯¹è¯æ¡†
class CServoDlg : public CDialogEx
{
// æž„造
// æž„造
public:
    CServoDlg(CWnd* pParent = NULL);    // æ ‡å‡†æž„造函数
    CServoDlg(CWnd* pParent = NULL);    // æ ‡å‡†æž„造函数
public:
@@ -50,16 +50,16 @@
    CPageLog*     m_pPageLog;
    CPageTransferLog* m_pPageTransferLog;
// å¯¹è¯æ¡†æ•°æ®
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_SERVO_DIALOG };
#endif
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV æ”¯æŒ
// å®žçް
// å®žçް
protected:
    HICON m_hIcon;
    COLORREF m_crBkgnd;
@@ -70,7 +70,7 @@
    CPanelAttributes* m_pPanelAttributes;
    CMyStatusbar* m_pMyStatusbar;
    // ç”Ÿæˆçš„æ¶ˆæ¯æ˜ å°„函数
    // ç”Ÿæˆçš„æ¶ˆæ¯æ˜ å°„函数
    virtual BOOL OnInitDialog();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
@@ -98,6 +98,8 @@
    afx_msg void OnUpdateMenuTestMessageSet(CCmdUI* pCmdUI);
    afx_msg void OnMenuTestMessageClear();
    afx_msg void OnUpdateMenuTestMessageClear(CCmdUI* pCmdUI);
    afx_msg void OnMenuToolsClientList();
    afx_msg void OnUpdateMenuToolsClientList(CCmdUI* pCmdUI);
    afx_msg void OnMenuHelpAbout();
    afx_msg void OnTimer(UINT_PTR nIDEvent);
    afx_msg LRESULT OnPanelResize(WPARAM wParam, LPARAM lParam);
SourceCode/Bond/Servo/TerminalDisplayDlg.cpp
@@ -104,7 +104,6 @@
void CTerminalDisplayDlg::Resize()
{
    CWnd* pItem;
    CRect rcClient, rcItem;
    GetClientRect(&rcClient);
    rcClient.top += 38;
SourceCode/Bond/Servo/ToolUnits.cpp
@@ -4,7 +4,9 @@
#include <memory>
#include <sstream>
#include <algorithm>
#include <ctime>
#include <iomanip>
#include <sstream>
CToolUnits::CToolUnits()
{
@@ -557,3 +559,19 @@
    std::snprintf(out, sizeof(out), "%s.%03d", date, ms);
    return out;
}
std::string CToolUnits::NowStrSec()
{
    using namespace std::chrono;
    auto now = system_clock::now();
    std::time_t t = system_clock::to_time_t(now);
    std::tm tm{};
#ifdef _WIN32
    localtime_s(&tm, &t);   // æœ¬åœ°æ—¶é—´ï¼ˆWindows çº¿ç¨‹å®‰å…¨ï¼‰
#else
    localtime_r(&t, &tm);   // æœ¬åœ°æ—¶é—´ï¼ˆPOSIX çº¿ç¨‹å®‰å…¨ï¼‰
#endif
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y%m%d%H%M%S"); // ä¾‹ï¼š2025-09-15 08:23:07
    return oss.str();
}
SourceCode/Bond/Servo/ToolUnits.h
@@ -59,5 +59,6 @@
    static std::string TimePointToUtcString(const std::optional<TP>& tp,
        const char* fmt = "%Y-%m-%d %H:%M:%S");
    static std::string TimePointToLocalStringMs(const std::optional<TP>& tp);
    static std::string NowStrSec();
};
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