chenluhua1980
2025-12-18 7d6d1769b7b76366fd20ab4358b8c10ce067d04f
1.生产数据统计链路已打通。待UI显示;
已添加3个文件
已修改6个文件
626 ■■■■■ 文件已修改
SourceCode/Bond/Servo/CPanelProduction.cpp 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPanelProduction.h 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Configuration.h 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ConfigurationProduction.cpp 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProductionStats.cpp 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ProductionStats.h 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Servo.vcxproj.filters 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/ServoDlg.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPanelProduction.cpp
@@ -1,4 +1,4 @@
// CPanelProduction.cpp
// CPanelProduction.cpp
//
#include "stdafx.h"
@@ -20,6 +20,8 @@
    m_hbrBkgnd = nullptr;
    m_nPanelWidth = 288;
    m_hPlaceholder = nullptr;
    m_bShiftSummaryValid = FALSE;
    m_pStatsThread = nullptr;
}
CPanelProduction::~CPanelProduction()
@@ -38,6 +40,7 @@
    ON_WM_SIZE()
    ON_NOTIFY(BYVERTICALLINE_MOVEX, IDC_LINE1, &CPanelProduction::OnVLineMoveX)
    ON_BN_CLICKED(IDC_BUTTON_CLOSE, &CPanelProduction::OnBnClickedButtonClose)
    ON_WM_TIMER()
END_MESSAGE_MAP()
int CPanelProduction::getPanelWidth()
@@ -58,6 +61,9 @@
    pLine1->SetBkgndColor(RGB(225, 225, 225));
    pLine1->SetLineColor(RGB(198, 198, 198));
    pLine1->EnableResize();
    SetTimer(1, 1000 * 10, nullptr);
    StartStatsThread();
    return TRUE;  // return TRUE unless you set the focus to a control
                  // Exception: OCX property pages should return FALSE
@@ -81,6 +87,8 @@
void CPanelProduction::OnDestroy()
{
    StopStatsThread();
    CDialogEx::OnDestroy();
    if (m_hbrBkgnd != nullptr) {
@@ -123,3 +131,68 @@
        pParent->PostMessage(WM_COMMAND, ID_MENU_WND_TEST_PANEL, 0);
    }
}
BOOL CPanelProduction::TryGetShiftSummary(ProductionShiftSummary& outSummary)
{
    CSingleLock lock(&m_csShiftSummary, TRUE);
    if (!m_bShiftSummaryValid) return FALSE;
    outSummary = m_shiftSummary;
    return TRUE;
}
void CPanelProduction::StartStatsThread()
{
    if (m_pStatsThread != nullptr) return;
    m_evStopStats.ResetEvent();
    m_pStatsThread = AfxBeginThread(&CPanelProduction::StatsThreadProc, this, THREAD_PRIORITY_BELOW_NORMAL, 0, 0);
    if (m_pStatsThread != nullptr) {
        m_pStatsThread->m_bAutoDelete = FALSE;
    }
}
void CPanelProduction::StopStatsThread()
{
    if (m_pStatsThread == nullptr) return;
    m_evStopStats.SetEvent();
    const DWORD rc = WaitForSingleObject(m_pStatsThread->m_hThread, 5000);
    if (rc == WAIT_OBJECT_0) {
        delete m_pStatsThread;
    }
    m_pStatsThread = nullptr;
}
UINT CPanelProduction::StatsThreadProc(LPVOID pParam)
{
    CPanelProduction* self = reinterpret_cast<CPanelProduction*>(pParam);
    if (self == nullptr) return 0;
    const DWORD intervalMs = 5000;
    for (;;) {
        if (self->m_evStopStats.Lock(intervalMs)) break;
        ProductionShiftSummary summary;
        if (ProductionStats::ComputeCurrentShiftSummary(theApp.m_model.m_configuration, summary)) {
            CSingleLock lock(&self->m_csShiftSummary, TRUE);
            self->m_shiftSummary = std::move(summary);
            self->m_bShiftSummaryValid = TRUE;
        }
    }
    return 0;
}
void CPanelProduction::OnTimer(UINT_PTR nIDEvent)
{
    // TODO: åœ¨æ­¤æ·»åŠ æ¶ˆæ¯å¤„ç†ç¨‹åºä»£ç å’Œ/或调用默认值
    if (nIDEvent == 1) {
        ProductionShiftSummary outSummary;
        if (TryGetShiftSummary(outSummary)) {
            TRACE("OnTimer outSummary.output.pairsPass:%d\n", outSummary.output.pairsPass);
        }
    }
    CDialogEx::OnTimer(nIDEvent);
}
SourceCode/Bond/Servo/CPanelProduction.h
@@ -1,5 +1,8 @@
#pragma once
#pragma once
#include "BlButton.h"
#include <afxmt.h>
#include "ProductionStats.h"
// CPanelProduction dialog
class CPanelProduction : public CDialogEx
@@ -19,10 +22,17 @@
    CBlButton m_btnClose;
    HWND m_hPlaceholder;
    // Production shift summary (updated by background thread)
    ProductionShiftSummary m_shiftSummary;
    BOOL m_bShiftSummaryValid;
    CCriticalSection m_csShiftSummary;
    CWinThread* m_pStatsThread;
    CEvent m_evStopStats;
protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
// å¯¹è¯æ¡†æ•°æ®
// å¯¹è¯æ¡†æ•°æ®
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_PANEL_PRODUCTION };
#endif
@@ -35,4 +45,14 @@
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg void OnVLineMoveX(NMHDR* nmhdr, LRESULT* result);
    afx_msg void OnBnClickedButtonClose();
    // Thread-safe snapshot for UI timer display
    BOOL TryGetShiftSummary(ProductionShiftSummary& outSummary);
private:
    static UINT AFX_CDECL StatsThreadProc(LPVOID pParam);
    void StartStatsThread();
    void StopStatsThread();
public:
    afx_msg void OnTimer(UINT_PTR nIDEvent);
};
SourceCode/Bond/Servo/Configuration.h
@@ -34,6 +34,13 @@
    int getPortCassetteSnSeed(int port);
    void setPortCassetteSnSeed(int port, int seed);
    // Production shift settings
    // Reads shift start times from ini.
    // - [Production] DayShiftStart=HH:MM (default 08:00)
    // - [Production] NightShiftStart=HH:MM (default DayShiftStart+12h)
    // Returns TRUE if both values are valid (or derived); otherwise FALSE and falls back to defaults.
    BOOL getProductionShiftStartMinutes(int& dayStartMinutes, int& nightStartMinutes);
public:
    void setP2RemoteEqReconnectInterval(int second);
    int getP2RemoteEqReconnectInterval();
SourceCode/Bond/Servo/ConfigurationProduction.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
#include "stdafx.h"
#include "Configuration.h"
#include <mutex>
#include <string>
#include <unordered_map>
static bool TryParseHHMM(const std::string& text, int& outMinutes)
{
    int hour = 0;
    int minute = 0;
    if (sscanf_s(text.c_str(), "%d:%d", &hour, &minute) != 2) return false;
    if (hour < 0 || hour >= 24) return false;
    if (minute < 0 || minute >= 60) return false;
    outMinutes = hour * 60 + minute;
    return true;
}
BOOL CConfiguration::getProductionShiftStartMinutes(int& dayStartMinutes, int& nightStartMinutes)
{
    struct CachedShift {
        BOOL ok = FALSE;
        int day = 0;
        int night = 0;
        bool inited = false;
    };
    static std::mutex s_mtx;
    static std::unordered_map<std::string, CachedShift> s_cache;
    const std::string filePath((LPCSTR)(LPCTSTR)m_strFilepath);
    {
        std::lock_guard<std::mutex> g(s_mtx);
        auto it = s_cache.find(filePath);
        if (it != s_cache.end() && it->second.inited) {
            dayStartMinutes = it->second.day;
            nightStartMinutes = it->second.night;
            return it->second.ok;
        }
    }
    char buf[64] = {};
    GetPrivateProfileStringA("Production", "DayShiftStart", "08:00", buf, (DWORD)sizeof(buf), m_strFilepath);
    std::string dayStr(buf);
    GetPrivateProfileStringA("Production", "NightShiftStart", "", buf, (DWORD)sizeof(buf), m_strFilepath);
    std::string nightStr(buf);
    const int kDefaultDay = 8 * 60;
    const int kDefaultNight = 20 * 60;
    bool okDay = TryParseHHMM(dayStr, dayStartMinutes);
    bool okNight = false;
    if (!nightStr.empty()) okNight = TryParseHHMM(nightStr, nightStartMinutes);
    if (!okDay) dayStartMinutes = kDefaultDay;
    if (!okNight) nightStartMinutes = (dayStartMinutes + 12 * 60) % (24 * 60);
    if (dayStartMinutes == nightStartMinutes) {
        dayStartMinutes = kDefaultDay;
        nightStartMinutes = kDefaultNight;
        {
            std::lock_guard<std::mutex> g(s_mtx);
            s_cache[filePath] = CachedShift{ FALSE, dayStartMinutes, nightStartMinutes, true };
        }
        return FALSE;
    }
    const BOOL ok = (okDay && (nightStr.empty() ? TRUE : okNight)) ? TRUE : FALSE;
    {
        std::lock_guard<std::mutex> g(s_mtx);
        s_cache[filePath] = CachedShift{ ok, dayStartMinutes, nightStartMinutes, true };
    }
    return ok;
}
SourceCode/Bond/Servo/ProductionStats.cpp
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
#include "stdafx.h"
#include "ProductionStats.h"
#include <algorithm>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include "Configuration.h"
#include "Log.h"
#include "sqlite3.h"
#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif
static std::string FormatLocal(const std::chrono::system_clock::time_point& tp)
{
    const std::time_t tt = std::chrono::system_clock::to_time_t(tp);
    std::tm tm{};
    localtime_s(&tm, &tt);
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return oss.str();
}
static std::string FormatUtcIso(const std::chrono::system_clock::time_point& tp)
{
    const std::time_t tt = std::chrono::system_clock::to_time_t(tp);
    std::tm tm{};
    gmtime_s(&tm, &tt);
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
    return oss.str();
}
static bool TryParseLocalTime(const std::string& text, std::chrono::system_clock::time_point& outTp)
{
    if (text.empty()) return false;
    std::tm tm{};
    std::istringstream iss(text);
    iss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
    if (iss.fail()) return false;
    tm.tm_isdst = -1;
    const std::time_t tt = mktime(&tm);
    if (tt == (time_t)-1) return false;
    outTp = std::chrono::system_clock::from_time_t(tt);
    return true;
}
static std::string GetExeDir()
{
    char path[MAX_PATH] = {};
    GetModuleFileNameA(nullptr, path, MAX_PATH);
    std::string exePath(path);
    const size_t pos = exePath.find_last_of("\\/");
    return (pos == std::string::npos) ? std::string() : exePath.substr(0, pos);
}
static bool FileExistsA(const std::string& path)
{
    const DWORD attr = GetFileAttributesA(path.c_str());
    return (attr != INVALID_FILE_ATTRIBUTES) && ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0);
}
static std::string PickDbPath(const std::string& rel1, const std::string& rel2)
{
    const std::string base = GetExeDir();
    const std::string p1 = base + "\\" + rel1;
    if (FileExistsA(p1)) return p1;
    return base + "\\" + rel2;
}
static void ComputeOutputFromProcessDb(
    const ProductionShiftWindow& win,
    ProductionOutputSummary& out)
{
    const std::string dbPath = PickDbPath("db\\process.db", "DB\\process.db");
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(dbPath.c_str(), &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return;
    }
    const char* sql =
        "SELECT class_id, buddy_id, aoi_result "
        "FROM glass_log "
        "WHERE t_end IS NOT NULL AND t_end >= ? AND t_end < ?;";
    sqlite3_stmt* stmt = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return;
    }
    sqlite3_bind_text(stmt, 1, win.startUtcIso.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 2, win.endUtcIso.c_str(), -1, SQLITE_TRANSIENT);
    struct PairAgg {
        bool hasPass = false;
        bool hasFail = false;
        bool hasNo = false;
    };
    std::unordered_map<std::string, PairAgg> pairs;
    for (;;) {
        const int rc = sqlite3_step(stmt);
        if (rc == SQLITE_ROW) {
            const char* classId = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
            const char* buddyId = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
            const int aoi = sqlite3_column_int(stmt, 2);
            const std::string a = classId ? classId : "";
            const std::string b = buddyId ? buddyId : "";
            std::string key;
            if (!b.empty()) {
                if (a <= b) key = a + "|" + b;
                else key = b + "|" + a;
            }
            else {
                key = a;
            }
            auto& agg = pairs[key];
            if (aoi == 1) agg.hasPass = true;
            else if (aoi == 2) agg.hasFail = true;
            else agg.hasNo = true;
        }
        else if (rc == SQLITE_DONE) {
            break;
        }
        else {
            break;
        }
    }
    sqlite3_finalize(stmt);
    sqlite3_close(db);
    out.pairsTotal = static_cast<long long>(pairs.size());
    for (const auto& kv : pairs) {
        const auto& agg = kv.second;
        if (agg.hasFail) out.pairsFail++;
        else if (agg.hasPass) out.pairsPass++;
        else out.pairsNoResult++;
    }
    const long long denom = out.pairsPass + out.pairsFail;
    out.yield = (denom > 0) ? (static_cast<double>(out.pairsPass) / static_cast<double>(denom)) : 0.0;
}
static void ComputeAlarmSummaryFromDb(
    const ProductionShiftWindow& win,
    ProductionAlarmSummary& out)
{
    const std::string dbPath = PickDbPath("DB\\AlarmManager.db", "DB\\AlarmManager.db");
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(dbPath.c_str(), &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return;
    }
    // 1) triggered within shift
    {
        const char* sql =
            "SELECT COUNT(1) FROM alarms "
            "WHERE start_time >= ? AND start_time < ?;";
        sqlite3_stmt* stmt = nullptr;
        if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
            sqlite3_bind_text(stmt, 1, win.startLocal.c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(stmt, 2, win.endLocal.c_str(), -1, SQLITE_TRANSIENT);
            if (sqlite3_step(stmt) == SQLITE_ROW) out.alarmsTriggered = sqlite3_column_int(stmt, 0);
            sqlite3_finalize(stmt);
        }
    }
    // 2) overlapping (including active)
    {
        const char* sql =
            "SELECT severity_level, start_time, end_time "
            "FROM alarms "
            "WHERE start_time < ? AND (end_time IS NULL OR end_time >= ?);";
        sqlite3_stmt* stmt = nullptr;
        if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
            sqlite3_bind_text(stmt, 1, win.endLocal.c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(stmt, 2, win.startLocal.c_str(), -1, SQLITE_TRANSIENT);
            const auto now = std::chrono::system_clock::now();
            for (;;) {
                const int rc = sqlite3_step(stmt);
                if (rc == SQLITE_ROW) {
                    const int severity = sqlite3_column_int(stmt, 0);
                    const char* sStart = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
                    const char* sEnd = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
                    std::chrono::system_clock::time_point aStart{};
                    if (!TryParseLocalTime(sStart ? sStart : "", aStart)) continue;
                    std::chrono::system_clock::time_point aEnd{};
                    bool hasEnd = TryParseLocalTime(sEnd ? sEnd : "", aEnd);
                    if (!hasEnd) aEnd = std::min(now, win.end);
                    const auto clipStart = std::max(aStart, win.start);
                    const auto clipEnd = std::min(aEnd, win.end);
                    if (clipEnd > clipStart) {
                        const auto secs = std::chrono::duration_cast<std::chrono::seconds>(clipEnd - clipStart).count();
                        out.downtimeMinutes += static_cast<double>(secs) / 60.0;
                    }
                    out.bySeverity[severity] += 1;
                    out.alarmsOverlapping += 1;
                }
                else if (rc == SQLITE_DONE) {
                    break;
                }
                else {
                    break;
                }
            }
            sqlite3_finalize(stmt);
        }
    }
    sqlite3_close(db);
}
static void ComputeTransferSummaryFromDb(
    const ProductionShiftWindow& win,
    ProductionTransferSummary& out)
{
    const std::string dbPath = PickDbPath("DB\\TransferManager.db", "DB\\TransferManager.db");
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(dbPath.c_str(), &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return;
    }
    const char* sql =
        "SELECT status, create_time, end_time "
        "FROM transfers "
        "WHERE end_time >= ? AND end_time < ? AND end_time != '';";
    sqlite3_stmt* stmt = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return;
    }
    sqlite3_bind_text(stmt, 1, win.startLocal.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 2, win.endLocal.c_str(), -1, SQLITE_TRANSIENT);
    long long totalSecs = 0;
    long long cntSecs = 0;
    for (;;) {
        const int rc = sqlite3_step(stmt);
        if (rc == SQLITE_ROW) {
            const char* sStatus = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
            const char* sCreate = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
            const char* sEnd = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
            const std::string status = sStatus ? sStatus : "";
            out.byStatus[status] += 1;
            out.transfersFinished += 1;
            std::chrono::system_clock::time_point tpCreate{}, tpEnd{};
            if (TryParseLocalTime(sCreate ? sCreate : "", tpCreate) && TryParseLocalTime(sEnd ? sEnd : "", tpEnd) && tpEnd > tpCreate) {
                totalSecs += std::chrono::duration_cast<std::chrono::seconds>(tpEnd - tpCreate).count();
                cntSecs += 1;
            }
        }
        else if (rc == SQLITE_DONE) {
            break;
        }
        else {
            break;
        }
    }
    sqlite3_finalize(stmt);
    sqlite3_close(db);
    out.avgCreateToEndSeconds = (cntSecs > 0) ? (static_cast<double>(totalSecs) / static_cast<double>(cntSecs)) : 0.0;
}
bool ProductionStats::GetCurrentShiftWindow(CConfiguration& config, ProductionShiftWindow& outWindow)
{
    int dayMin = 8 * 60;
    int nightMin = 20 * 60;
    config.getProductionShiftStartMinutes(dayMin, nightMin);
    const auto now = std::chrono::system_clock::now();
    const std::time_t ttNow = std::chrono::system_clock::to_time_t(now);
    std::tm tmNow{};
    localtime_s(&tmNow, &ttNow);
    std::tm tmMid = tmNow;
    tmMid.tm_hour = 0;
    tmMid.tm_min = 0;
    tmMid.tm_sec = 0;
    tmMid.tm_isdst = -1;
    const std::time_t ttMid = mktime(&tmMid);
    if (ttMid == (time_t)-1) return false;
    const auto midnight = std::chrono::system_clock::from_time_t(ttMid);
    const auto startDayToday = midnight + std::chrono::minutes(dayMin);
    const auto startNightToday = midnight + std::chrono::minutes(nightMin);
    const auto startDay = (now >= startDayToday) ? startDayToday : (startDayToday - std::chrono::hours(24));
    const auto startNight = (now >= startNightToday) ? startNightToday : (startNightToday - std::chrono::hours(24));
    ProductionShiftType type = ProductionShiftType::Day;
    auto start = startDay;
    if (startNight > startDay) {
        type = ProductionShiftType::Night;
        start = startNight;
    }
    const int durationMin =
        (type == ProductionShiftType::Day)
        ? ((nightMin - dayMin + 24 * 60) % (24 * 60))
        : ((dayMin - nightMin + 24 * 60) % (24 * 60));
    if (durationMin <= 0) return false;
    outWindow.type = type;
    outWindow.start = start;
    outWindow.end = start + std::chrono::minutes(durationMin);
    outWindow.startLocal = FormatLocal(outWindow.start);
    outWindow.endLocal = FormatLocal(outWindow.end);
    outWindow.startUtcIso = FormatUtcIso(outWindow.start);
    outWindow.endUtcIso = FormatUtcIso(outWindow.end);
    return true;
}
bool ProductionStats::ComputeCurrentShiftSummary(CConfiguration& config, ProductionShiftSummary& outSummary)
{
    ProductionShiftWindow win;
    if (!GetCurrentShiftWindow(config, win)) return false;
    outSummary = ProductionShiftSummary{};
    outSummary.window = win;
    ComputeOutputFromProcessDb(win, outSummary.output);
    ComputeAlarmSummaryFromDb(win, outSummary.alarms);
    ComputeTransferSummaryFromDb(win, outSummary.transfers);
    return true;
}
void ProductionStats::LogCurrentShiftSummary(CConfiguration& config)
{
    ProductionShiftSummary s;
    if (!ComputeCurrentShiftSummary(config, s)) {
        LOGE("<ProductionStats>Failed to compute shift summary.");
        return;
    }
    const char* shiftName = (s.window.type == ProductionShiftType::Day) ? "Day" : "Night";
    LOGI("<ProductionStats>Shift=%s, [%s ~ %s]", shiftName, s.window.startLocal.c_str(), s.window.endLocal.c_str());
    LOGI("<ProductionStats>Output(pairs): total=%lld, pass=%lld, fail=%lld, no_result=%lld, yield=%.2f%%",
        s.output.pairsTotal, s.output.pairsPass, s.output.pairsFail, s.output.pairsNoResult, s.output.yield * 100.0);
    LOGI("<ProductionStats>Alarms: triggered=%d, overlapping=%d, downtime=%.1f min",
        s.alarms.alarmsTriggered, s.alarms.alarmsOverlapping, s.alarms.downtimeMinutes);
    if (!s.alarms.bySeverity.empty()) {
        std::ostringstream oss;
        oss << "<ProductionStats>AlarmsBySeverity:";
        for (const auto& kv : s.alarms.bySeverity) oss << " L" << kv.first << "=" << kv.second;
        LOGI("%s", oss.str().c_str());
    }
    LOGI("<ProductionStats>Transfers: finished=%d, avg(create->end)=%.1fs", s.transfers.transfersFinished, s.transfers.avgCreateToEndSeconds);
    if (!s.transfers.byStatus.empty()) {
        std::ostringstream oss;
        oss << "<ProductionStats>TransfersByStatus:";
        for (const auto& kv : s.transfers.byStatus) oss << " " << kv.first << "=" << kv.second;
        LOGI("%s", oss.str().c_str());
    }
}
SourceCode/Bond/Servo/ProductionStats.h
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
#pragma once
#include <chrono>
#include <map>
#include <string>
class CConfiguration;
enum class ProductionShiftType : int {
    Day = 1,
    Night = 2
};
struct ProductionShiftWindow {
    ProductionShiftType type{ ProductionShiftType::Day };
    std::chrono::system_clock::time_point start{};
    std::chrono::system_clock::time_point end{};
    std::string startLocal;   // "YYYY-MM-DD HH:MM:SS"
    std::string endLocal;     // "YYYY-MM-DD HH:MM:SS"
    std::string startUtcIso;  // "YYYY-MM-DDTHH:MM:SSZ"
    std::string endUtcIso;    // "YYYY-MM-DDTHH:MM:SSZ"
};
struct ProductionOutputSummary {
    long long pairsTotal = 0;        // "对数"
    long long pairsPass = 0;
    long long pairsFail = 0;
    long long pairsNoResult = 0;
    double yield = 0.0;              // pairsPass / (pairsPass + pairsFail), 0 if denom==0
};
struct ProductionAlarmSummary {
    int alarmsTriggered = 0;         // start_time within shift window
    int alarmsOverlapping = 0;       // overlaps shift window (including active)
    double downtimeMinutes = 0.0;    // overlap minutes (best-effort)
    std::map<int, int> bySeverity;   // severity_level -> count (overlapping)
};
struct ProductionTransferSummary {
    int transfersFinished = 0;        // end_time within shift window
    std::map<std::string, int> byStatus;
    double avgCreateToEndSeconds = 0.0;
};
struct ProductionShiftSummary {
    ProductionShiftWindow window;
    ProductionOutputSummary output;
    ProductionAlarmSummary alarms;
    ProductionTransferSummary transfers;
};
class ProductionStats {
public:
    static bool GetCurrentShiftWindow(CConfiguration& config, ProductionShiftWindow& outWindow);
    static bool ComputeCurrentShiftSummary(CConfiguration& config, ProductionShiftSummary& outSummary);
    static void LogCurrentShiftSummary(CConfiguration& config);
};
SourceCode/Bond/Servo/Servo.vcxproj
@@ -246,6 +246,7 @@
    <ClInclude Include="CPageReport.h" />
    <ClInclude Include="CPageVarialbles.h" />
    <ClInclude Include="CPanelProduction.h" />
    <ClInclude Include="ProductionStats.h" />
    <ClInclude Include="CParam.h" />
    <ClInclude Include="CCjPage1.h" />
    <ClInclude Include="CProcessDataListDlg.h" />
@@ -467,7 +468,9 @@
    <ClCompile Include="CPageLinkSignal.cpp" />
    <ClCompile Include="CPageReport.cpp" />
    <ClCompile Include="CPageVarialbles.cpp" />
    <ClCompile Include="ConfigurationProduction.cpp" />
    <ClCompile Include="CPanelProduction.cpp" />
    <ClCompile Include="ProductionStats.cpp" />
    <ClCompile Include="CParam.cpp" />
    <ClCompile Include="CCjPage1.cpp" />
    <ClCompile Include="CProcessDataListDlg.cpp" />
SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -236,7 +236,9 @@
    <ClCompile Include="CVariableEditDlg2.cpp" />
    <ClCompile Include="CEventEditDlg.cpp" />
    <ClCompile Include="CReportEditDlg.cpp" />
    <ClCompile Include="ConfigurationProduction.cpp" />
    <ClCompile Include="CPanelProduction.cpp" />
    <ClCompile Include="ProductionStats.cpp" />
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="AlarmManager.h" />
@@ -258,6 +260,7 @@
    <ClInclude Include="stdafx.h" />
    <ClInclude Include="targetver.h" />
    <ClInclude Include="TerminalDisplayDlg.h" />
    <ClInclude Include="ProductionStats.h" />
    <ClInclude Include="SECSRuntimeManager.h" />
    <ClInclude Include="CCLinkPerformance\CCLinkIEControl.h">
      <Filter>CCLinkPerformance</Filter>
SourceCode/Bond/Servo/ServoDlg.cpp
@@ -708,7 +708,7 @@
void CServoDlg::OnUpdateMenuWndTestPanel(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(TRUE);
    pCmdUI->SetRadio(m_nLeftPanelType == 1);
    pCmdUI->SetCheck(m_nLeftPanelType == 1);
}
void CServoDlg::OnMenuWndProPanel()
@@ -719,7 +719,7 @@
void CServoDlg::OnUpdateMenuWndProPanel(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(TRUE);
    pCmdUI->SetRadio(m_nLeftPanelType == 2);
    pCmdUI->SetCheck(m_nLeftPanelType == 2);
}
void CServoDlg::OnMenuHelpAbout()