From 7d6d1769b7b76366fd20ab4358b8c10ce067d04f Mon Sep 17 00:00:00 2001
From: chenluhua1980 <Chenluhua@qq.com>
Date: 星期四, 18 十二月 2025 12:04:54 +0800
Subject: [PATCH] 1.生产数据统计链路已打通。待UI显示;

---
 SourceCode/Bond/Servo/Configuration.h             |    7 
 SourceCode/Bond/Servo/Servo.vcxproj               |    5 
 SourceCode/Bond/Servo/Servo.vcxproj.filters       |    5 
 SourceCode/Bond/Servo/ProductionStats.cpp         |  377 ++++++++++++++++++++++++++++++++++
 SourceCode/Bond/Servo/ProductionStats.h           |   58 +++++
 SourceCode/Bond/Servo/CPanelProduction.cpp        |   75 ++++++
 SourceCode/Bond/Servo/ConfigurationProduction.cpp |   75 ++++++
 SourceCode/Bond/Servo/ServoDlg.cpp                |    4 
 SourceCode/Bond/Servo/CPanelProduction.h          |   24 ++
 9 files changed, 623 insertions(+), 7 deletions(-)

diff --git a/SourceCode/Bond/Servo/CPanelProduction.cpp b/SourceCode/Bond/Servo/CPanelProduction.cpp
index 0f01b3f..bbc5900 100644
--- a/SourceCode/Bond/Servo/CPanelProduction.cpp
+++ b/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: 鍦ㄦ娣诲姞娑堟伅澶勭悊绋嬪簭浠g爜鍜�/鎴栬皟鐢ㄩ粯璁ゅ��
+	if (nIDEvent == 1) {
+		ProductionShiftSummary outSummary;
+		if (TryGetShiftSummary(outSummary)) {
+			TRACE("OnTimer outSummary.output.pairsPass:%d\n", outSummary.output.pairsPass);
+		}
+	}
+
+	CDialogEx::OnTimer(nIDEvent);
+}
diff --git a/SourceCode/Bond/Servo/CPanelProduction.h b/SourceCode/Bond/Servo/CPanelProduction.h
index 60713f3..527db4f 100644
--- a/SourceCode/Bond/Servo/CPanelProduction.h
+++ b/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);
 };
diff --git a/SourceCode/Bond/Servo/Configuration.h b/SourceCode/Bond/Servo/Configuration.h
index 0bf2628..9c6aaf7 100644
--- a/SourceCode/Bond/Servo/Configuration.h
+++ b/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();
diff --git a/SourceCode/Bond/Servo/ConfigurationProduction.cpp b/SourceCode/Bond/Servo/ConfigurationProduction.cpp
new file mode 100644
index 0000000..3aa9bcc
--- /dev/null
+++ b/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;
+}
diff --git a/SourceCode/Bond/Servo/ProductionStats.cpp b/SourceCode/Bond/Servo/ProductionStats.cpp
new file mode 100644
index 0000000..aff2a74
--- /dev/null
+++ b/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());
+	}
+}
diff --git a/SourceCode/Bond/Servo/ProductionStats.h b/SourceCode/Bond/Servo/ProductionStats.h
new file mode 100644
index 0000000..84c8cb5
--- /dev/null
+++ b/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);
+};
+
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj b/SourceCode/Bond/Servo/Servo.vcxproj
index 252010a..ddcddc3 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj
+++ b/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" />
@@ -659,4 +662,4 @@
     <Error Condition="!Exists('..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
     <Error Condition="!Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240803.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240803.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
   </Target>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj.filters b/SourceCode/Bond/Servo/Servo.vcxproj.filters
index 21925a5..652252b 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.filters
+++ b/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>
@@ -560,4 +563,4 @@
       <UniqueIdentifier>{885738f6-3122-4bb9-8308-46b7f692fb13}</UniqueIdentifier>
     </Filter>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/SourceCode/Bond/Servo/ServoDlg.cpp b/SourceCode/Bond/Servo/ServoDlg.cpp
index 248c5af..89aaf91 100644
--- a/SourceCode/Bond/Servo/ServoDlg.cpp
+++ b/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()

--
Gitblit v1.9.3