fix(beacon): 修复RSSI滑动窗口检测逻辑 - 窗口大小改用max(enter/exit.windowSize) - 进入/退出检测分别使用各自的windowSize子窗口 - 到岗确认时清理RSSI窗口避免历史脏数据
This commit is contained in:
@@ -57,10 +57,10 @@ public class RssiSlidingWindowDetector {
|
|||||||
/**
|
/**
|
||||||
* 执行检测(强进弱出算法)
|
* 执行检测(强进弱出算法)
|
||||||
*
|
*
|
||||||
* @param window RSSI 滑动窗口样本(从旧到新)
|
* @param window RSSI 滑动窗口样本(从旧到新)
|
||||||
* @param enterConfig 进入(到岗)判定配置 - 强阈值
|
* @param enterConfig 进入(到岗)判定配置 - 强阈值
|
||||||
* @param exitConfig 退出(离岗)判定配置 - 弱阈值
|
* @param exitConfig 退出(离岗)判定配置 - 弱阈值
|
||||||
* @param currentState 当前状态
|
* @param currentState 当前状态
|
||||||
* @return 检测结果
|
* @return 检测结果
|
||||||
*/
|
*/
|
||||||
public DetectionResult detect(
|
public DetectionResult detect(
|
||||||
@@ -74,24 +74,26 @@ public class RssiSlidingWindowDetector {
|
|||||||
return DetectionResult.NO_CHANGE;
|
return DetectionResult.NO_CHANGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 统计满足强阈值的样本数(用于进入判定)
|
// 3. 状态转换判定(使用各自的窗口大小)
|
||||||
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. 状态转换判定
|
|
||||||
if (currentState == AreaState.OUT_AREA) {
|
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()) {
|
if (enterHits >= enterConfig.getHitCount()) {
|
||||||
log.info("[RssiDetector] 进入条件满足:enterHits={}, required={}",
|
log.info("[RssiDetector] 进入条件满足:enterHits={}, required={}",
|
||||||
enterHits, enterConfig.getHitCount());
|
enterHits, enterConfig.getHitCount());
|
||||||
@@ -99,6 +101,24 @@ public class RssiSlidingWindowDetector {
|
|||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (exitHits >= exitConfig.getHitCount()) {
|
||||||
log.info("[RssiDetector] 离开条件满足:exitHits={}, required={}",
|
log.info("[RssiDetector] 离开条件满足:exitHits={}, required={}",
|
||||||
exitHits, exitConfig.getHitCount());
|
exitHits, exitConfig.getHitCount());
|
||||||
@@ -109,12 +129,26 @@ public class RssiSlidingWindowDetector {
|
|||||||
return DetectionResult.NO_CHANGE;
|
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
|
* 从蓝牙设备列表中提取目标信标的 RSSI
|
||||||
*
|
*
|
||||||
* @param bluetoothDevices 蓝牙设备列表
|
* @param bluetoothDevices 蓝牙设备列表
|
||||||
* 格式:[{"rssi":-52,"type":243,"mac":"F0:C8:60:1D:10:BB"},...]
|
* 格式:[{"rssi":-52,"type":243,"mac":"F0:C8:60:1D:10:BB"},...]
|
||||||
* @param targetMac 目标信标 MAC 地址
|
* @param targetMac 目标信标 MAC 地址
|
||||||
* @return RSSI 值,如果未找到返回 -999(缺失值)
|
* @return RSSI 值,如果未找到返回 -999(缺失值)
|
||||||
*/
|
*/
|
||||||
public Integer extractTargetRssi(Object bluetoothDevices, String targetMac) {
|
public Integer extractTargetRssi(Object bluetoothDevices, String targetMac) {
|
||||||
@@ -124,8 +158,7 @@ public class RssiSlidingWindowDetector {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<java.util.Map<String, Object>> deviceList =
|
List<java.util.Map<String, Object>> deviceList = (List<java.util.Map<String, Object>>) bluetoothDevices;
|
||||||
(List<java.util.Map<String, Object>>) bluetoothDevices;
|
|
||||||
|
|
||||||
return deviceList.stream()
|
return deviceList.stream()
|
||||||
.filter(device -> targetMac.equals(device.get("mac")))
|
.filter(device -> targetMac.equals(device.get("mac")))
|
||||||
@@ -143,8 +176,8 @@ public class RssiSlidingWindowDetector {
|
|||||||
* <p>
|
* <p>
|
||||||
* 窗口大小必须 >= 配置的窗口大小
|
* 窗口大小必须 >= 配置的窗口大小
|
||||||
*
|
*
|
||||||
* @param window 窗口样本
|
* @param window 窗口样本
|
||||||
* @param windowSize 配置的窗口大小
|
* @param windowSize 配置的窗口大小
|
||||||
* @return true - 有效,false - 无效
|
* @return true - 有效,false - 无效
|
||||||
*/
|
*/
|
||||||
public boolean isWindowValid(List<Integer> window, int windowSize) {
|
public boolean isWindowValid(List<Integer> window, int windowSize) {
|
||||||
|
|||||||
@@ -102,9 +102,11 @@ public class BeaconDetectionRuleProcessor {
|
|||||||
log.debug("[BeaconDetection] 提取RSSI:deviceId={}, beaconMac={}, rssi={}",
|
log.debug("[BeaconDetection] 提取RSSI:deviceId={}, beaconMac={}, rssi={}",
|
||||||
deviceId, beaconConfig.getBeaconMac(), targetRssi);
|
deviceId, beaconConfig.getBeaconMac(), targetRssi);
|
||||||
|
|
||||||
// 4. 更新滑动窗口
|
// 4. 更新滑动窗口(使用 enter 和 exit 中较大的窗口大小)
|
||||||
BeaconPresenceConfig.WindowConfig windowConfig = beaconConfig.getWindow();
|
int maxWindowSize = Math.max(
|
||||||
windowRedisDAO.updateWindow(deviceId, areaId, targetRssi, windowConfig.getSampleTtlSeconds());
|
beaconConfig.getEnter().getWindowSize(),
|
||||||
|
beaconConfig.getExit().getWindowSize());
|
||||||
|
windowRedisDAO.updateWindow(deviceId, areaId, targetRssi, maxWindowSize);
|
||||||
|
|
||||||
// 5. 获取当前窗口样本
|
// 5. 获取当前窗口样本
|
||||||
List<Integer> window = windowRedisDAO.getWindow(deviceId, areaId);
|
List<Integer> window = windowRedisDAO.getWindow(deviceId, areaId);
|
||||||
@@ -151,7 +153,11 @@ public class BeaconDetectionRuleProcessor {
|
|||||||
// 2. 清除离岗记录(如果存在)
|
// 2. 清除离岗记录(如果存在)
|
||||||
signalLossRedisDAO.clearLossRecord(deviceId, areaId);
|
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);
|
Integer currentRssi = window.isEmpty() ? -999 : window.get(window.size() - 1);
|
||||||
|
|
||||||
// 4. 构建触发数据
|
// 4. 构建触发数据
|
||||||
|
|||||||
Reference in New Issue
Block a user