| ¶Ô±ÈÐÂÎļþ |
| | |
| | | #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()); |
| | | } |
| | | } |