#include "rule_engine.h" #include #include #include #include #include #include "utils/logger.h" namespace rk3588 { namespace { bool SafeLocalTime(std::time_t t, std::tm& out) { #if defined(_WIN32) return localtime_s(&out, &t) == 0; #elif defined(__unix__) || defined(__APPLE__) return localtime_r(&t, &out) != nullptr; #else static std::mutex mu; std::lock_guard lock(mu); std::tm* p = std::localtime(&t); if (!p) return false; out = *p; return true; #endif } } // namespace bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector& labels) { labels_ = labels; rules_.clear(); duration_start_.clear(); last_trigger_.clear(); if (!rules_config.IsArray()) { LogError("[RuleEngine] rules must be an array"); return false; } for (const auto& rule_json : rules_config.AsArray()) { AlarmRule rule; rule.name = rule_json.ValueOr("name", "unnamed_rule"); rule.min_duration_ms = rule_json.ValueOr("min_duration_ms", 0); rule.cooldown_ms = rule_json.ValueOr("cooldown_ms", 5000); rule.schedule = rule_json.ValueOr("schedule", ""); // Parse class_ids if (const SimpleJson* ids = rule_json.Find("class_ids")) { for (const auto& id_val : ids->AsArray()) { rule.class_ids.insert(id_val.AsInt(-1)); } } // Parse objects (class names) and convert to class_ids if (const SimpleJson* objects = rule_json.Find("objects")) { for (const auto& obj_val : objects->AsArray()) { std::string obj_name = obj_val.AsString(""); for (size_t i = 0; i < labels_.size(); ++i) { if (labels_[i] == obj_name) { rule.class_ids.insert(static_cast(i)); break; } } } } // Parse ROI if (const SimpleJson* roi_json = rule_json.Find("roi")) { rule.roi.x = roi_json->ValueOr("x", 0.0f); rule.roi.y = roi_json->ValueOr("y", 0.0f); rule.roi.w = roi_json->ValueOr("w", 1.0f); rule.roi.h = roi_json->ValueOr("h", 1.0f); } rules_.push_back(rule); { std::ostringstream oss; oss << "[RuleEngine] loaded rule: " << rule.name << " class_ids=" << rule.class_ids.size() << " roi=(" << rule.roi.x << "," << rule.roi.y << "," << rule.roi.w << "," << rule.roi.h << ")" << " min_duration=" << rule.min_duration_ms << "ms" << " cooldown=" << rule.cooldown_ms << "ms"; LogInfo(oss.str()); } } return true; } RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr& frame) { RuleMatchResult result; if (!frame || !frame->det) return result; const auto& detections = frame->det->items; int img_w = frame->det->img_w > 0 ? frame->det->img_w : frame->width; int img_h = frame->det->img_h > 0 ? frame->det->img_h : frame->height; for (auto& rule : rules_) { // Check schedule if (!rule.schedule.empty() && !IsInSchedule(rule.schedule)) { duration_start_.erase(rule.name); continue; } // Check cooldown if (!CheckCooldown(rule.name)) { continue; } // Find matching detections std::vector matched; for (const auto& det : detections) { // Check class_id (if empty, match all) if (!rule.class_ids.empty() && rule.class_ids.find(det.cls_id) == rule.class_ids.end()) { continue; } // Check ROI if (!IsInRoi(det.bbox, rule.roi, img_w, img_h)) { continue; } matched.push_back(det); } bool currently_matched = !matched.empty(); // Check duration if (CheckDuration(rule.name, currently_matched)) { result.matched = true; result.rule_name = rule.name; result.matched_detections = matched; TriggerCooldown(rule.name); duration_start_.erase(rule.name); return result; } } return result; } void RuleEngine::Reset() { duration_start_.clear(); last_trigger_.clear(); } bool RuleEngine::IsInRoi(const Rect& bbox, const RoiRect& roi, int img_w, int img_h) const { // Convert bbox to normalized coordinates float bbox_cx = (bbox.x + bbox.w / 2.0f) / img_w; float bbox_cy = (bbox.y + bbox.h / 2.0f) / img_h; // Check if center is in ROI return bbox_cx >= roi.x && bbox_cx <= (roi.x + roi.w) && bbox_cy >= roi.y && bbox_cy <= (roi.y + roi.h); } bool RuleEngine::IsInSchedule(const std::string& schedule) const { // Parse "HH:MM-HH:MM" format if (schedule.length() != 11 || schedule[5] != '-') { return true; // Invalid format, assume always active } int start_h = 0, start_m = 0, end_h = 0, end_m = 0; (void)std::sscanf(schedule.c_str(), "%d:%d-%d:%d", &start_h, &start_m, &end_h, &end_m); std::time_t now = std::time(nullptr); std::tm local{}; if (!SafeLocalTime(now, local)) { return true; } int curr_min = local.tm_hour * 60 + local.tm_min; int start_min = start_h * 60 + start_m; int end_min = end_h * 60 + end_m; if (start_min <= end_min) { return curr_min >= start_min && curr_min <= end_min; } else { // Overnight schedule (e.g., "22:00-06:00") return curr_min >= start_min || curr_min <= end_min; } } bool RuleEngine::CheckDuration(const std::string& rule_name, bool currently_matched) { auto it = std::find_if(rules_.begin(), rules_.end(), [&](const AlarmRule& r) { return r.name == rule_name; }); if (it == rules_.end()) return false; int min_duration_ms = it->min_duration_ms; auto now = std::chrono::steady_clock::now(); if (!currently_matched) { duration_start_.erase(rule_name); return false; } auto start_it = duration_start_.find(rule_name); if (start_it == duration_start_.end()) { duration_start_[rule_name] = now; if (min_duration_ms <= 0) { return true; // No duration requirement } return false; } auto elapsed = std::chrono::duration_cast( now - start_it->second); return elapsed.count() >= min_duration_ms; } bool RuleEngine::CheckCooldown(const std::string& rule_name) { auto it = std::find_if(rules_.begin(), rules_.end(), [&](const AlarmRule& r) { return r.name == rule_name; }); if (it == rules_.end()) return false; auto last_it = last_trigger_.find(rule_name); if (last_it == last_trigger_.end()) { return true; // Never triggered } auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast( now - last_it->second); return elapsed.count() >= it->cooldown_ms; } void RuleEngine::TriggerCooldown(const std::string& rule_name) { last_trigger_[rule_name] = std::chrono::steady_clock::now(); } } // namespace rk3588