fix(beacon): 修复RSSI滑动窗口检测逻辑 - 窗口大小改用max(enter/exit.windowSize) - 进入/退出检测分别使用各自的windowSize子窗口 - 到岗确认时清理RSSI窗口避免历史脏数据
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

This commit is contained in:
lzh
2026-01-30 10:24:20 +08:00
parent f818641b1a
commit dbf96e521a
2 changed files with 69 additions and 30 deletions

View File

@@ -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<Integer> 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<Integer> 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<Integer> getSubWindow(List<Integer> 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<java.util.Map<String, Object>> deviceList =
(List<java.util.Map<String, Object>>) bluetoothDevices;
List<java.util.Map<String, Object>> deviceList = (List<java.util.Map<String, Object>>) bluetoothDevices;
return deviceList.stream()
.filter(device -> targetMac.equals(device.get("mac")))
@@ -143,8 +176,8 @@ public class RssiSlidingWindowDetector {
* <p>
* 窗口大小必须 >= 配置的窗口大小
*
* @param window 窗口样本
* @param windowSize 配置的窗口大小
* @param window 窗口样本
* @param windowSize 配置的窗口大小
* @return true - 有效false - 无效
*/
public boolean isWindowValid(List<Integer> window, int windowSize) {

View File

@@ -102,9 +102,11 @@ public class BeaconDetectionRuleProcessor {
log.debug("[BeaconDetection] 提取RSSIdeviceId={}, 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<Integer> 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. 构建触发数据