From dbf96e521ada270ebe8232b3278254cb88e84de0 Mon Sep 17 00:00:00 2001 From: lzh Date: Fri, 30 Jan 2026 10:24:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(beacon):=20=E4=BF=AE=E5=A4=8DRSSI=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E7=AA=97=E5=8F=A3=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= =?UTF-8?q?=20-=20=E7=AA=97=E5=8F=A3=E5=A4=A7=E5=B0=8F=E6=94=B9=E7=94=A8ma?= =?UTF-8?q?x(enter/exit.windowSize)=20-=20=E8=BF=9B=E5=85=A5/=E9=80=80?= =?UTF-8?q?=E5=87=BA=E6=A3=80=E6=B5=8B=E5=88=86=E5=88=AB=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=90=84=E8=87=AA=E7=9A=84windowSize=E5=AD=90=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=20-=20=E5=88=B0=E5=B2=97=E7=A1=AE=E8=AE=A4=E6=97=B6?= =?UTF-8?q?=E6=B8=85=E7=90=86RSSI=E7=AA=97=E5=8F=A3=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=84=8F=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detector/RssiSlidingWindowDetector.java | 85 +++++++++++++------ .../BeaconDetectionRuleProcessor.java | 14 ++- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/detector/RssiSlidingWindowDetector.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/detector/RssiSlidingWindowDetector.java index 6d5552c..5ee8673 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/detector/RssiSlidingWindowDetector.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/detector/RssiSlidingWindowDetector.java @@ -57,10 +57,10 @@ public class RssiSlidingWindowDetector { /** * 执行检测(强进弱出算法) * - * @param window RSSI 滑动窗口样本(从旧到新) - * @param enterConfig 进入(到岗)判定配置 - 强阈值 - * @param exitConfig 退出(离岗)判定配置 - 弱阈值 - * @param currentState 当前状态 + * @param window RSSI 滑动窗口样本(从旧到新) + * @param enterConfig 进入(到岗)判定配置 - 强阈值 + * @param exitConfig 退出(离岗)判定配置 - 弱阈值 + * @param currentState 当前状态 * @return 检测结果 */ public DetectionResult detect( @@ -74,24 +74,26 @@ public class RssiSlidingWindowDetector { return DetectionResult.NO_CHANGE; } - // 1. 统计满足强阈值的样本数(用于进入判定) - long enterHits = window.stream() - .filter(rssi -> rssi >= enterConfig.getRssiThreshold()) - .count(); - - // 2. 统计满足弱阈值的样本数(用于退出判定) - // 条件:RSSI < 弱阈值 或 为缺失值(-999) - long exitHits = window.stream() - .filter(rssi -> rssi < exitConfig.getWeakRssiThreshold() || rssi == -999) - .count(); - - log.debug("[RssiDetector] 检测统计:currentState={}, windowSize={}, enterHits={}/{}, exitHits={}/{}", - currentState, window.size(), enterHits, enterConfig.getHitCount(), - exitHits, exitConfig.getHitCount()); - - // 3. 状态转换判定 + // 3. 状态转换判定(使用各自的窗口大小) if (currentState == AreaState.OUT_AREA) { // 当前在区域外,检测是否进入 + // 使用 enter.windowSize 取最近的样本子窗口 + int enterWindowSize = enterConfig.getWindowSize(); + List enterWindow = getSubWindow(window, enterWindowSize); + + if (enterWindow.size() < enterWindowSize) { + log.debug("[RssiDetector] 进入窗口样本不足:current={}, required={}", + enterWindow.size(), enterWindowSize); + return DetectionResult.NO_CHANGE; + } + + long enterHits = enterWindow.stream() + .filter(rssi -> rssi >= enterConfig.getRssiThreshold()) + .count(); + + log.debug("[RssiDetector] 进入检测:windowSize={}, enterHits={}/{}", + enterWindow.size(), enterHits, enterConfig.getHitCount()); + if (enterHits >= enterConfig.getHitCount()) { log.info("[RssiDetector] 进入条件满足:enterHits={}, required={}", enterHits, enterConfig.getHitCount()); @@ -99,6 +101,24 @@ public class RssiSlidingWindowDetector { } } else { // 当前在区域内,检测是否离开 + // 使用 exit.windowSize 取最近的样本子窗口 + int exitWindowSize = exitConfig.getWindowSize(); + List exitWindow = getSubWindow(window, exitWindowSize); + + if (exitWindow.size() < exitWindowSize) { + log.debug("[RssiDetector] 退出窗口样本不足:current={}, required={}", + exitWindow.size(), exitWindowSize); + return DetectionResult.NO_CHANGE; + } + + // 条件:RSSI < 弱阈值 或 为缺失值(-999) + long exitHits = exitWindow.stream() + .filter(rssi -> rssi < exitConfig.getWeakRssiThreshold() || rssi == -999) + .count(); + + log.debug("[RssiDetector] 退出检测:windowSize={}, exitHits={}/{}", + exitWindow.size(), exitHits, exitConfig.getHitCount()); + if (exitHits >= exitConfig.getHitCount()) { log.info("[RssiDetector] 离开条件满足:exitHits={}, required={}", exitHits, exitConfig.getHitCount()); @@ -109,12 +129,26 @@ public class RssiSlidingWindowDetector { return DetectionResult.NO_CHANGE; } + /** + * 获取窗口的子窗口(最近 N 个样本) + * + * @param window 完整窗口 + * @param size 子窗口大小 + * @return 子窗口(最近的样本) + */ + private List getSubWindow(List window, int size) { + if (window.size() <= size) { + return window; + } + return window.subList(window.size() - size, window.size()); + } + /** * 从蓝牙设备列表中提取目标信标的 RSSI * * @param bluetoothDevices 蓝牙设备列表 - * 格式:[{"rssi":-52,"type":243,"mac":"F0:C8:60:1D:10:BB"},...] - * @param targetMac 目标信标 MAC 地址 + * 格式:[{"rssi":-52,"type":243,"mac":"F0:C8:60:1D:10:BB"},...] + * @param targetMac 目标信标 MAC 地址 * @return RSSI 值,如果未找到返回 -999(缺失值) */ public Integer extractTargetRssi(Object bluetoothDevices, String targetMac) { @@ -124,8 +158,7 @@ public class RssiSlidingWindowDetector { try { @SuppressWarnings("unchecked") - List> deviceList = - (List>) bluetoothDevices; + List> deviceList = (List>) bluetoothDevices; return deviceList.stream() .filter(device -> targetMac.equals(device.get("mac"))) @@ -143,8 +176,8 @@ public class RssiSlidingWindowDetector { *

* 窗口大小必须 >= 配置的窗口大小 * - * @param window 窗口样本 - * @param windowSize 配置的窗口大小 + * @param window 窗口样本 + * @param windowSize 配置的窗口大小 * @return true - 有效,false - 无效 */ public boolean isWindowValid(List window, int windowSize) { diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java index ee1bd2a..72a9d79 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java @@ -102,9 +102,11 @@ public class BeaconDetectionRuleProcessor { log.debug("[BeaconDetection] 提取RSSI:deviceId={}, beaconMac={}, rssi={}", deviceId, beaconConfig.getBeaconMac(), targetRssi); - // 4. 更新滑动窗口 - BeaconPresenceConfig.WindowConfig windowConfig = beaconConfig.getWindow(); - windowRedisDAO.updateWindow(deviceId, areaId, targetRssi, windowConfig.getSampleTtlSeconds()); + // 4. 更新滑动窗口(使用 enter 和 exit 中较大的窗口大小) + int maxWindowSize = Math.max( + beaconConfig.getEnter().getWindowSize(), + beaconConfig.getExit().getWindowSize()); + windowRedisDAO.updateWindow(deviceId, areaId, targetRssi, maxWindowSize); // 5. 获取当前窗口样本 List window = windowRedisDAO.getWindow(deviceId, areaId); @@ -151,7 +153,11 @@ public class BeaconDetectionRuleProcessor { // 2. 清除离岗记录(如果存在) signalLossRedisDAO.clearLossRecord(deviceId, areaId); - // 3. 获取当前最新的 RSSI 值 + // 3. 清理 RSSI 窗口(避免历史脏数据影响新的在岗周期) + windowRedisDAO.clearWindow(deviceId, areaId); + log.debug("[BeaconDetection] 到岗时清理RSSI窗口:deviceId={}, areaId={}", deviceId, areaId); + + // 4. 获取当前最新的 RSSI 值(使用原窗口快照,因为已清理) Integer currentRssi = window.isEmpty() ? -999 : window.get(window.size() - 1); // 4. 构建触发数据