safesight-edge/plugins/alarm/alarm_node.cpp

368 lines
14 KiB
C++

#include <atomic>
#include <chrono>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include "node.h"
#include "rule_engine.h"
#include "frame_ring_buffer.h"
#include "actions/action_base.h"
#include "actions/log_action.h"
#include "actions/http_action.h"
#include "actions/snapshot_action.h"
#include "actions/clip_action.h"
namespace rk3588 {
class AlarmNode : public INode {
private:
struct AlarmJob {
AlarmEvent event;
std::shared_ptr<Frame> frame;
};
public:
std::string Id() const override { return id_; }
std::string Type() const override { return "alarm"; }
bool Init(const SimpleJson& config, const NodeContext& ctx) override {
id_ = config.ValueOr<std::string>("id", "alarm");
// Parse labels for class name mapping
if (const SimpleJson* labels_cfg = config.Find("labels")) {
for (const auto& label : labels_cfg->AsArray()) {
labels_.push_back(label.AsString(""));
}
}
// Initialize rule engine
if (const SimpleJson* rules_cfg = config.Find("rules")) {
if (!rule_engine_.Init(*rules_cfg, labels_)) {
std::cerr << "[alarm] failed to init rule engine\n";
return false;
}
}
// Get pre/post buffer settings from clip action config
int pre_sec = 5;
int post_sec = 0;
int fps_hint = 25;
if (const SimpleJson* actions_cfg = config.Find("actions")) {
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
pre_sec = clip_cfg->ValueOr<int>("pre_sec", 5);
post_sec = clip_cfg->ValueOr<int>("post_sec", 0);
fps_hint = clip_cfg->ValueOr<int>("fps", 25);
}
}
frame_buffer_ = std::make_shared<FrameRingBuffer>(pre_sec, post_sec, fps_hint);
// Alarm worker queue (avoid blocking Process())
size_t alarm_q_size = 32;
QueueDropStrategy alarm_q_strategy = QueueDropStrategy::DropOldest;
if (const SimpleJson* q = config.Find("event_queue")) {
if (q->IsObject()) {
alarm_q_size = static_cast<size_t>(q->ValueOr<int>("size", static_cast<int>(alarm_q_size)));
const std::string strat = q->ValueOr<std::string>("strategy", "drop_oldest");
if (strat == "drop_newest") alarm_q_strategy = QueueDropStrategy::DropNewest;
else if (strat == "block") alarm_q_strategy = QueueDropStrategy::Block;
else alarm_q_strategy = QueueDropStrategy::DropOldest;
}
}
alarm_queue_size_ = alarm_q_size;
alarm_queue_strategy_ = alarm_q_strategy;
alarm_queue_ = std::make_shared<SpscQueue<AlarmJob>>(alarm_queue_size_, alarm_queue_strategy_);
// Initialize actions
if (const SimpleJson* actions_cfg = config.Find("actions")) {
// Log action
if (const SimpleJson* log_cfg = actions_cfg->Find("log")) {
if (log_cfg->ValueOr<bool>("enable", true)) {
auto action = std::make_unique<LogAction>();
if (action->Init(*log_cfg)) {
actions_.push_back(std::move(action));
}
}
}
// Snapshot action (must be before HTTP to fill snapshot_url)
if (const SimpleJson* snap_cfg = actions_cfg->Find("snapshot")) {
if (snap_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<SnapshotAction>();
if (action->Init(*snap_cfg)) {
actions_.push_back(std::move(action));
}
}
}
// Clip action (must be before HTTP to fill clip_url)
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
if (clip_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<ClipAction>();
if (action->Init(*clip_cfg)) {
auto* clip_ptr = static_cast<ClipAction*>(action.get());
clip_ptr->SetFrameBuffer(frame_buffer_);
actions_.push_back(std::move(action));
}
}
}
// HTTP action (should be last to include media URLs)
if (const SimpleJson* http_cfg = actions_cfg->Find("http")) {
if (http_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<HttpAction>();
if (action->Init(*http_cfg)) {
actions_.push_back(std::move(action));
}
}
}
} else {
// Default: just log action
auto action = std::make_unique<LogAction>();
SimpleJson empty_cfg;
action->Init(empty_cfg);
actions_.push_back(std::move(action));
}
input_queue_ = ctx.input_queue;
if (!input_queue_) {
std::cerr << "[alarm] no input queue for node " << id_ << "\n";
return false;
}
std::cout << "[alarm] initialized with " << actions_.size() << " actions\n";
return true;
}
bool Start() override {
worker_running_.store(true);
worker_ = std::thread(&AlarmNode::WorkerLoop, this);
std::cout << "[alarm] started\n";
return true;
}
void Stop() override {
worker_running_.store(false);
if (alarm_queue_) alarm_queue_->Stop();
if (worker_.joinable()) worker_.join();
// Ensure all actions fully stop (Drain only clears pending work; Stop must reclaim threads/resources).
for (auto& action : actions_) action->Drain();
for (auto& action : actions_) action->Stop();
std::cout << "[alarm] stopped, processed " << processed_frames_
<< " frames, triggered " << alarm_count_ << " alarms\n";
}
void Drain() override {
// First drain alarm jobs (so snapshot/clip are finished before draining HTTP queue).
while (alarm_queue_ && (alarm_queue_->Size() > 0 || in_flight_.load() > 0)) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
for (auto& action : actions_) action->Drain();
}
bool UpdateConfig(const SimpleJson& new_config) override {
const std::string new_id = new_config.ValueOr<std::string>("id", id_);
if (!new_id.empty() && new_id != id_) {
return false;
}
// Parse labels
std::vector<std::string> new_labels;
if (const SimpleJson* labels_cfg = new_config.Find("labels")) {
for (const auto& label : labels_cfg->AsArray()) {
new_labels.push_back(label.AsString(""));
}
}
// Build new rule engine
RuleEngine new_engine;
if (const SimpleJson* rules_cfg = new_config.Find("rules")) {
if (!new_engine.Init(*rules_cfg, new_labels)) {
return false;
}
}
// Pre/post buffer settings
int pre_sec = 5;
int post_sec = 0;
int fps_hint = 25;
if (const SimpleJson* actions_cfg = new_config.Find("actions")) {
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
pre_sec = clip_cfg->ValueOr<int>("pre_sec", 5);
post_sec = clip_cfg->ValueOr<int>("post_sec", 0);
fps_hint = clip_cfg->ValueOr<int>("fps", 25);
}
}
auto new_frame_buffer = std::make_shared<FrameRingBuffer>(pre_sec, post_sec, fps_hint);
// Initialize new actions
std::vector<std::unique_ptr<IAlarmAction>> new_actions;
if (const SimpleJson* actions_cfg = new_config.Find("actions")) {
if (const SimpleJson* log_cfg = actions_cfg->Find("log")) {
if (log_cfg->ValueOr<bool>("enable", true)) {
auto action = std::make_unique<LogAction>();
if (action->Init(*log_cfg)) {
new_actions.push_back(std::move(action));
}
}
}
if (const SimpleJson* snap_cfg = actions_cfg->Find("snapshot")) {
if (snap_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<SnapshotAction>();
if (action->Init(*snap_cfg)) {
new_actions.push_back(std::move(action));
}
}
}
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
if (clip_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<ClipAction>();
if (action->Init(*clip_cfg)) {
auto* clip_ptr = static_cast<ClipAction*>(action.get());
clip_ptr->SetFrameBuffer(new_frame_buffer);
new_actions.push_back(std::move(action));
}
}
}
if (const SimpleJson* http_cfg = actions_cfg->Find("http")) {
if (http_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<HttpAction>();
if (action->Init(*http_cfg)) {
new_actions.push_back(std::move(action));
}
}
}
}
if (new_actions.empty()) {
auto action = std::make_unique<LogAction>();
SimpleJson empty_cfg;
if (action->Init(empty_cfg)) {
new_actions.push_back(std::move(action));
}
}
// Stop worker thread to avoid executing actions while swapping.
std::shared_ptr<SpscQueue<AlarmJob>> old_queue;
std::thread old_worker;
{
std::lock_guard<std::mutex> lock(mu_);
worker_running_.store(false);
old_queue = alarm_queue_;
old_worker = std::move(worker_);
}
if (old_queue) old_queue->Stop();
if (old_worker.joinable()) old_worker.join();
std::vector<std::unique_ptr<IAlarmAction>> old_actions;
{
std::lock_guard<std::mutex> lock(mu_);
labels_ = std::move(new_labels);
rule_engine_ = std::move(new_engine);
frame_buffer_ = std::move(new_frame_buffer);
old_actions = std::move(actions_);
actions_ = std::move(new_actions);
in_flight_.store(0);
alarm_queue_ = std::make_shared<SpscQueue<AlarmJob>>(alarm_queue_size_, alarm_queue_strategy_);
worker_running_.store(true);
worker_ = std::thread(&AlarmNode::WorkerLoop, this);
}
for (auto& action : old_actions) action->Drain();
for (auto& action : old_actions) action->Stop();
return true;
}
bool GetCustomMetrics(SimpleJson& out) const override {
std::lock_guard<std::mutex> lock(mu_);
SimpleJson::Object o;
o["alarm_total"] = SimpleJson(static_cast<double>(alarm_count_));
o["processed"] = SimpleJson(static_cast<double>(processed_frames_));
out = SimpleJson(std::move(o));
return true;
}
NodeStatus Process(FramePtr frame) override {
if (!frame) return NodeStatus::DROP;
std::lock_guard<std::mutex> lock(mu_);
if (frame_buffer_) {
using namespace std::chrono;
const uint64_t ts_ms = static_cast<uint64_t>(duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
frame_buffer_->Push(frame, ts_ms);
}
auto result = rule_engine_.Evaluate(frame);
if (result.matched) TriggerAlarm(result, frame);
++processed_frames_;
return NodeStatus::OK;
}
private:
void WorkerLoop() {
while (worker_running_.load()) {
AlarmJob job;
if (!alarm_queue_) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
if (!alarm_queue_->Pop(job, std::chrono::milliseconds(100))) {
continue;
}
in_flight_.fetch_add(1);
for (auto& action : actions_) {
action->Execute(job.event, job.frame);
}
in_flight_.fetch_sub(1);
}
}
void TriggerAlarm(const RuleMatchResult& result, FramePtr frame) {
++alarm_count_;
AlarmEvent event;
event.node_id = id_;
event.rule_name = result.rule_name;
event.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
event.frame_id = frame->frame_id;
event.detections = result.matched_detections;
AlarmJob job;
job.event = std::move(event);
job.frame = std::move(frame);
if (alarm_queue_) {
alarm_queue_->Push(std::move(job));
}
}
std::string id_;
std::vector<std::string> labels_;
RuleEngine rule_engine_;
std::shared_ptr<FrameRingBuffer> frame_buffer_;
std::vector<std::unique_ptr<IAlarmAction>> actions_;
mutable std::mutex mu_;
std::shared_ptr<SpscQueue<AlarmJob>> alarm_queue_;
size_t alarm_queue_size_ = 32;
QueueDropStrategy alarm_queue_strategy_ = QueueDropStrategy::DropOldest;
std::atomic<bool> worker_running_{false};
std::thread worker_;
std::atomic<size_t> in_flight_{0};
std::shared_ptr<SpscQueue<FramePtr>> input_queue_;
uint64_t processed_frames_ = 0;
uint64_t alarm_count_ = 0;
};
REGISTER_NODE(AlarmNode, "alarm");
} // namespace rk3588