#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, "
|
"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<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 char* sStart = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
|
const char* sEnd = reinterpret_cast<const char*>(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<std::chrono::seconds>(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<long long>(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<double>(out.pairsPass) / static_cast<double>(denom)) : 0.0;
|
out.taktSamplePairs = cntTakt;
|
out.avgTaktSeconds = (cntTakt > 0) ? (static_cast<double>(sumTakt) / static_cast<double>(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<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>Takt: avg=%.1fs, samples=%lld", s.output.avgTaktSeconds, s.output.taktSamplePairs);
|
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());
|
}
|
}
|
|
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;
|
}
|