15. 事件过滤与聚合:事件过滤器模式、事件聚合器、事件窗口计算

好,咱们今天聊点实战中特别常用的东西——事件过滤与聚合。

你想想看,一个嵌入式系统跑起来,事件那是满天飞。按键按下、传感器数据、定时器超时、网络包到达……如果每个事件都去处理,CPU 早就累趴下了。更关键的是,很多事件其实是「噪音」,或者需要攒一批再处理才有意义。

我个人习惯把这类问题拆成三个模式来解决:事件过滤器事件聚合器事件窗口计算。它们各司其职,组合起来能搞定 90% 的复杂场景。

15.1 事件过滤器模式:把噪音挡在门外

说白了,过滤器就是一道门。符合条件的事件放进来,不符合的直接丢掉。

我在项目中遇到过这样一个坑:一个温度传感器,每 100ms 上报一次数据。但传感器本身有噪声,偶尔会跳变到离谱的值,比如 25°C 突然跳到 85°C。如果不加过滤,系统就会误判为过热,触发报警甚至关机。

核心思路:过滤器不改变事件内容,只做「通过/丢弃」的决策。

常见的过滤策略有几种:

  • 阈值过滤:超过某个范围直接丢弃。比如温度 > 100°C 或 < -40°C 视为无效。
  • 变化率过滤:相邻两次事件差值过大,可能是噪声,先丢一次看看。
  • 重复过滤:短时间内收到相同的事件,只保留第一个。

来看一个简单的过滤器实现:

// 事件过滤器:基于阈值 + 变化率
typedef struct {
    int32_t last_valid_value;
    uint32_t last_tick;
    int32_t max_change_rate;   // 每 tick 最大允许变化
    int32_t min_threshold;
    int32_t max_threshold;
} event_filter_t;

int event_filter_apply(event_filter_t *filter, int32_t value, uint32_t tick) {
    // 阈值检查
    if (value < filter->min_threshold || value > filter->max_threshold) {
        return 0;  // 丢弃
    }
    // 变化率检查
    uint32_t dt = tick - filter->last_tick;
    if (dt > 0) {
        int32_t delta = value - filter->last_valid_value;
        if (delta < 0) delta = -delta;
        if (delta / dt > filter->max_change_rate) {
            return 0;  // 变化太快,可能是噪声
        }
    }
    // 更新状态
    filter->last_valid_value = value;
    filter->last_tick = tick;
    return 1;  // 通过
}

我的经验:过滤器的参数别写死。我习惯把它们做成可配置的,放在结构体里,这样调试阶段可以动态调整阈值,不用重新编译。

15.2 事件聚合器:把零散事件攒成一批

有些场景下,单个事件没什么用,攒够一批才有意义。比如:

  • 按键去抖:连续 5 次采样都是低电平,才认为按键真的按下了。
  • 数据打包:收到 10 个传感器数据后,统一发一次网络包。
  • 状态确认:连续 3 次报警事件,才触发真正的报警动作。

事件聚合器的核心就是「等」——等数量够了,或者等时间到了。

// 事件聚合器:按数量聚合
typedef struct {
    uint32_t count_threshold;   // 触发阈值
    uint32_t current_count;
    uint32_t timeout_ms;        // 超时时间(防止永远等不到)
    uint32_t start_tick;
    void (*on_aggregated)(void *context);
    void *context;
} event_aggregator_t;

int event_aggregator_push(event_aggregator_t *agg, uint32_t tick) {
    if (agg->current_count == 0) {
        agg->start_tick = tick;
    }
    agg->current_count++;

    // 检查是否达到阈值
    if (agg->current_count >= agg->count_threshold) {
        agg->on_aggregated(agg->context);
        agg->current_count = 0;
        return 1;
    }
    // 检查是否超时
    if (tick - agg->start_tick > agg->timeout_ms) {
        // 超时了,把当前这批也处理掉(虽然没攒够)
        agg->on_aggregated(agg->context);
        agg->current_count = 0;
        return 1;
    }
    return 0;
}

注意:聚合器一定要加超时机制。我曾经在一个项目中忘了加,结果某个事件源突然停了,聚合器永远等不到阈值,数据就卡死了。嗯,那次排查花了我大半天。

15.3 事件窗口计算:滑动窗口里的统计

窗口计算比聚合更高级一点。它不光攒事件,还要在窗口内做统计——比如最近 1 秒内的平均值、最大值、最小值。

我举个例子你就明白了:

  • 你想知道过去 5 分钟内 CPU 的平均负载。
  • 你想检测最近 10 次采样中是否有超过 3 次异常。
  • 你想计算最近 100ms 内接收到的网络包数量。

这些都需要一个「滑动窗口」来维护历史数据。

// 滑动窗口计算器
#define WINDOW_SIZE 100

typedef struct {
    int32_t buffer[WINDOW_SIZE];
    uint32_t head;
    uint32_t count;
    int32_t sum;       // 用于快速计算平均值
    int32_t max_value;
    int32_t min_value;
} sliding_window_t;

void window_push(sliding_window_t *w, int32_t value) {
    // 如果窗口满了,先移除最旧的数据
    if (w->count == WINDOW_SIZE) {
        int32_t old = w->buffer[w->head];
        w->sum -= old;
        // 如果移除的是最大值或最小值,需要重新扫描
        // (这里为了简洁,省略了重新扫描的逻辑)
    } else {
        w->count++;
    }
    // 插入新数据
    w->buffer[w->head] = value;
    w->head = (w->head + 1) % WINDOW_SIZE;
    w->sum += value;

    // 更新最大最小值
    if (value > w->max_value) w->max_value = value;
    if (value < w->min_value) w->min_value = value;
}

int32_t window_average(sliding_window_t *w) {
    if (w->count == 0) return 0;
    return w->sum / w->count;
}

性能提示:窗口越大,维护最大最小值的代价越高。如果窗口很大(比如 1000 个元素),我建议用「双端队列 + 单调栈」来优化,而不是每次重新扫描。

15.4 三种模式如何配合使用

在实际项目中,这三种模式很少单独出现。它们通常串成一条流水线:

  1. 原始事件 → 先经过过滤器,去掉噪声和无效事件。
  2. 干净事件 → 进入聚合器,攒够一批或者超时后触发。
  3. 批量事件 → 送入窗口计算器,做统计或趋势分析。

举个例子,一个工业振动监测系统:

  • 传感器每 10ms 上报一次振动幅度。
  • 过滤器先去掉超过 ±100g 的异常值(可能是传感器脱落)。
  • 聚合器每收到 100 个有效数据(即 1 秒),打包成一个批次。
  • 窗口计算器维护最近 10 个批次(即 10 秒)的 RMS 值,判断是否超过报警阈值。

你看,三个模式一组合,一个完整的监测逻辑就出来了。

我的习惯:我会把过滤器、聚合器、窗口计算器都做成「可插拔」的模块。每个模块只依赖事件结构体,不依赖具体业务。这样换一个项目,代码基本不用改,改改配置参数就行。

15.5 一张图看懂事件处理流水线

下面这张图展示了从原始事件到最终输出的完整流程:

事件处理流水线:过滤 → 聚合 → 窗口计算 原始事件流 传感器/按键/网络 事件过滤器 阈值 / 变化率 / 去重 丢弃噪声,保留有效 事件聚合器 按数量 / 按时间 攒批触发 窗口计算 滑动窗口统计 均值/极值/计数 输出 丢弃(不满足条件) 超时触发(即使未攒够) 实际案例:振动监测系统 ① 传感器每 10ms 上报振动幅度 → ② 过滤器去掉 ±100g 外的异常值 ③ 聚合器每 100 个有效数据打包一次 → ④ 窗口计算最近 10 个批次的 RMS 值

15.6 避坑指南与最佳实践

最后,分享几个我踩过的坑:

  • 过滤器太严格:我曾经把变化率阈值设得太小,结果正常的数据也被丢了。系统看起来「很安静」,其实已经出问题了。建议先用日志模式跑一段时间,看看过滤掉了多少数据,再调整参数。
  • 聚合器忘记超时:前面说过了,不加超时,事件源一停就死锁。一定要加,而且超时时间要合理——太短会频繁触发,太长会延迟响应。
  • 窗口计算的内存占用:如果窗口很大(比如 10000 个元素),每个元素是 4 字节,那就是 40KB。在资源受限的 MCU 上,这可能直接撑爆 RAM。我建议用环形缓冲区 + 分段统计来降低内存需求。
  • 优先级反转:如果过滤器和聚合器运行在不同的任务中,要注意优先级。过滤器通常跑在中断或高优先级任务里,聚合器可以跑在低优先级任务里。别让聚合器阻塞了过滤器的处理。

总结一下:事件过滤与聚合,说白了就是「去粗取精」和「化零为整」。过滤器帮你挡住垃圾,聚合器帮你攒够弹药,窗口计算帮你看出趋势。三个模式配合好,你的事件处理系统就会既高效又可靠。

嗯,今天就聊到这里。这些模式我在多个项目里反复用过,每次都能让代码结构清晰不少。你可以在自己的项目里试试,从最简单的过滤器开始,慢慢加上聚合和窗口计算,你会发现事件处理变得特别顺手。