#include "stdafx.h" #include "ProductionStats.h" #include #include #include #include #include #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, " "IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_start, 'localtime'), '')," "IFNULL(strftime('%Y-%m-%d %H:%M:%S', t_end, 'localtime'), '') " "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; long long maxTaktSeconds = -1; }; std::unordered_map pairs; for (;;) { const int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* classId = reinterpret_cast(sqlite3_column_text(stmt, 0)); const char* buddyId = reinterpret_cast(sqlite3_column_text(stmt, 1)); const int aoi = sqlite3_column_int(stmt, 2); const char* sStart = reinterpret_cast(sqlite3_column_text(stmt, 3)); const char* sEnd = reinterpret_cast(sqlite3_column_text(stmt, 4)); 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; std::chrono::system_clock::time_point tpStart{}, tpEnd{}; if (TryParseLocalTime(sStart ? sStart : "", tpStart) && TryParseLocalTime(sEnd ? sEnd : "", tpEnd) && tpEnd > tpStart) { const auto secs = std::chrono::duration_cast(tpEnd - tpStart).count(); if (secs > agg.maxTaktSeconds) agg.maxTaktSeconds = secs; } } else if (rc == SQLITE_DONE) { break; } else { break; } } sqlite3_finalize(stmt); sqlite3_close(db); out.pairsTotal = static_cast(pairs.size()); long long sumTakt = 0; long long cntTakt = 0; for (const auto& kv : pairs) { const auto& agg = kv.second; if (agg.hasFail) out.pairsFail++; else if (agg.hasPass) out.pairsPass++; else out.pairsNoResult++; if (agg.maxTaktSeconds >= 0) { sumTakt += agg.maxTaktSeconds; cntTakt += 1; } } const long long denom = out.pairsPass + out.pairsFail; out.yield = (denom > 0) ? (static_cast(out.pairsPass) / static_cast(denom)) : 0.0; out.taktSamplePairs = cntTakt; out.avgTaktSeconds = (cntTakt > 0) ? (static_cast(sumTakt) / static_cast(cntTakt)) : 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(sqlite3_column_text(stmt, 1)); const char* sEnd = reinterpret_cast(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(clipEnd - clipStart).count(); out.downtimeMinutes += static_cast(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(sqlite3_column_text(stmt, 0)); const char* sCreate = reinterpret_cast(sqlite3_column_text(stmt, 1)); const char* sEnd = reinterpret_cast(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(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(totalSecs) / static_cast(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("Failed to compute shift summary."); return; } const char* shiftName = (s.window.type == ProductionShiftType::Day) ? "Day" : "Night"; LOGI("Shift=%s, [%s ~ %s]", shiftName, s.window.startLocal.c_str(), s.window.endLocal.c_str()); LOGI("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("Takt: avg=%.1fs, samples=%lld", s.output.avgTaktSeconds, s.output.taktSamplePairs); LOGI("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 << "AlarmsBySeverity:"; for (const auto& kv : s.alarms.bySeverity) oss << " L" << kv.first << "=" << kv.second; LOGI("%s", oss.str().c_str()); } LOGI("Transfers: finished=%d, avg(create->end)=%.1fs", s.transfers.transfersFinished, s.transfers.avgCreateToEndSeconds); if (!s.transfers.byStatus.empty()) { std::ostringstream oss; oss << "TransfersByStatus:"; for (const auto& kv : s.transfers.byStatus) oss << " " << kv.first << "=" << kv.second; LOGI("%s", oss.str().c_str()); } } bool ProductionStats::ComputeDayNightSummaries(CConfiguration& config, ProductionShiftSummary& outDay, ProductionShiftSummary& outNight) { ProductionShiftSummary cur; if (!ComputeCurrentShiftSummary(config, cur)) return false; // Determine previous adjacent window for the other shift. ProductionShiftSummary other = cur; if (cur.window.type == ProductionShiftType::Day) { other.window.type = ProductionShiftType::Night; other.window.end = cur.window.start; other.window.start = cur.window.start - (cur.window.end - cur.window.start); } else { other.window.type = ProductionShiftType::Day; other.window.end = cur.window.start; other.window.start = cur.window.start - (cur.window.end - cur.window.start); } other.window.startLocal = FormatLocal(other.window.start); other.window.endLocal = FormatLocal(other.window.end); other.window.startUtcIso = FormatUtcIso(other.window.start); other.window.endUtcIso = FormatUtcIso(other.window.end); other.output = ProductionOutputSummary{}; other.alarms = ProductionAlarmSummary{}; other.transfers = ProductionTransferSummary{}; ComputeOutputFromProcessDb(other.window, other.output); ComputeAlarmSummaryFromDb(other.window, other.alarms); ComputeTransferSummaryFromDb(other.window, other.transfers); if (cur.window.type == ProductionShiftType::Day) { outDay = std::move(cur); outNight = std::move(other); } else { outNight = std::move(cur); outDay = std::move(other); } return true; }