-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Expand file tree
/
Copy pathsec_weapon_detect.rs
More file actions
419 lines (380 loc) · 15.1 KB
/
sec_weapon_detect.rs
File metadata and controls
419 lines (380 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Concealed metallic object detection — ADR-041 Category 2 Security module.
//!
//! Detects concealed metallic objects via differential CSI multipath signatures.
//! Metal has significantly higher RF reflectivity than human tissue, producing
//! characteristic amplitude variance / phase variance ratios. This module is
//! research-grade and experimental — it requires calibration for each deployment
//! environment.
//!
//! The detection principle: when a person carrying a metallic object moves through
//! the sensing area, the multipath signature shows a higher amplitude-to-phase
//! variance ratio compared to a person without metal, because metal strongly
//! reflects RF energy while producing less phase dispersion than diffuse tissue.
//!
//! Events: METAL_ANOMALY(220), WEAPON_ALERT(221), CALIBRATION_NEEDED(222).
//! Budget: S (<5 ms).
#[cfg(not(feature = "std"))]
use libm::{fabsf, sqrtf};
#[cfg(feature = "std")]
fn sqrtf(x: f32) -> f32 { x.sqrt() }
#[cfg(feature = "std")]
fn fabsf(x: f32) -> f32 { x.abs() }
const MAX_SC: usize = 32;
/// Calibration frames (5 seconds at 20 Hz).
const BASELINE_FRAMES: u32 = 100;
/// Amplitude variance / phase variance ratio threshold for metal detection.
const METAL_RATIO_THRESH: f32 = 4.0;
/// Elevated ratio for weapon-grade alert (very high reflectivity).
const WEAPON_RATIO_THRESH: f32 = 8.0;
/// Minimum motion energy to consider detection valid (ignore static scenes).
const MIN_MOTION_ENERGY: f32 = 0.5;
/// Minimum presence required (person must be present).
const MIN_PRESENCE: i32 = 1;
/// Consecutive frames for metal anomaly debounce.
const METAL_DEBOUNCE: u8 = 4;
/// Consecutive frames for weapon alert debounce.
const WEAPON_DEBOUNCE: u8 = 6;
/// Cooldown frames after event emission.
const COOLDOWN: u16 = 60;
/// Re-calibration trigger: if baseline drift exceeds this ratio.
const RECALIB_DRIFT_THRESH: f32 = 3.0;
/// Window for running variance computation.
const VAR_WINDOW: usize = 16;
pub const EVENT_METAL_ANOMALY: i32 = 220;
pub const EVENT_WEAPON_ALERT: i32 = 221;
pub const EVENT_CALIBRATION_NEEDED: i32 = 222;
/// Concealed metallic object detector.
pub struct WeaponDetector {
/// Baseline amplitude variance per subcarrier.
baseline_amp_var: [f32; MAX_SC],
/// Baseline phase variance per subcarrier.
baseline_phase_var: [f32; MAX_SC],
/// Calibration: sum of amplitude values.
cal_amp_sum: [f32; MAX_SC],
cal_amp_sq_sum: [f32; MAX_SC],
/// Calibration: sum of phase values.
cal_phase_sum: [f32; MAX_SC],
cal_phase_sq_sum: [f32; MAX_SC],
cal_count: u32,
calibrated: bool,
/// Rolling amplitude window per subcarrier (flattened: MAX_SC * VAR_WINDOW).
amp_window: [f32; MAX_SC],
/// Rolling phase window per subcarrier.
phase_window: [f32; MAX_SC],
/// Running amplitude variance (Welford online).
run_amp_mean: [f32; MAX_SC],
run_amp_m2: [f32; MAX_SC],
/// Running phase variance (Welford online).
run_phase_mean: [f32; MAX_SC],
run_phase_m2: [f32; MAX_SC],
run_count: u32,
/// Debounce counters.
metal_run: u8,
weapon_run: u8,
/// Cooldowns.
cd_metal: u16,
cd_weapon: u16,
cd_recalib: u16,
frame_count: u32,
}
impl WeaponDetector {
pub const fn new() -> Self {
Self {
baseline_amp_var: [0.0; MAX_SC],
baseline_phase_var: [0.0; MAX_SC],
cal_amp_sum: [0.0; MAX_SC],
cal_amp_sq_sum: [0.0; MAX_SC],
cal_phase_sum: [0.0; MAX_SC],
cal_phase_sq_sum: [0.0; MAX_SC],
cal_count: 0,
calibrated: false,
amp_window: [0.0; MAX_SC],
phase_window: [0.0; MAX_SC],
run_amp_mean: [0.0; MAX_SC],
run_amp_m2: [0.0; MAX_SC],
run_phase_mean: [0.0; MAX_SC],
run_phase_m2: [0.0; MAX_SC],
run_count: 0,
metal_run: 0,
weapon_run: 0,
cd_metal: 0,
cd_weapon: 0,
cd_recalib: 0,
frame_count: 0,
}
}
/// Process one CSI frame. Returns `(event_id, value)` pairs.
pub fn process_frame(
&mut self,
phases: &[f32],
amplitudes: &[f32],
variance: &[f32],
motion_energy: f32,
presence: i32,
) -> &[(i32, f32)] {
let n_sc = phases.len().min(amplitudes.len()).min(variance.len()).min(MAX_SC);
if n_sc < 2 {
return &[];
}
self.frame_count += 1;
self.cd_metal = self.cd_metal.saturating_sub(1);
self.cd_weapon = self.cd_weapon.saturating_sub(1);
self.cd_recalib = self.cd_recalib.saturating_sub(1);
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
let mut ne = 0usize;
// Calibration phase: collect baseline statistics in empty room.
if !self.calibrated {
for i in 0..n_sc {
self.cal_amp_sum[i] += amplitudes[i];
self.cal_amp_sq_sum[i] += amplitudes[i] * amplitudes[i];
self.cal_phase_sum[i] += phases[i];
self.cal_phase_sq_sum[i] += phases[i] * phases[i];
}
self.cal_count += 1;
if self.cal_count >= BASELINE_FRAMES {
let n = self.cal_count as f32;
for i in 0..n_sc {
let amp_mean = self.cal_amp_sum[i] / n;
self.baseline_amp_var[i] =
(self.cal_amp_sq_sum[i] / n - amp_mean * amp_mean).max(0.001);
let phase_mean = self.cal_phase_sum[i] / n;
self.baseline_phase_var[i] =
(self.cal_phase_sq_sum[i] / n - phase_mean * phase_mean).max(0.001);
}
self.calibrated = true;
}
return unsafe { &EVENTS[..0] };
}
// Update running Welford statistics.
self.run_count += 1;
let rc = self.run_count as f32;
for i in 0..n_sc {
// Amplitude Welford.
let delta_a = amplitudes[i] - self.run_amp_mean[i];
self.run_amp_mean[i] += delta_a / rc;
let delta2_a = amplitudes[i] - self.run_amp_mean[i];
self.run_amp_m2[i] += delta_a * delta2_a;
// Phase Welford.
let delta_p = phases[i] - self.run_phase_mean[i];
self.run_phase_mean[i] += delta_p / rc;
let delta2_p = phases[i] - self.run_phase_mean[i];
self.run_phase_m2[i] += delta_p * delta2_p;
}
// Only detect when someone is present and moving.
if presence < MIN_PRESENCE || motion_energy < MIN_MOTION_ENERGY {
self.metal_run = 0;
self.weapon_run = 0;
// Reset running stats periodically when no one is present.
if self.run_count > 200 {
self.run_count = 0;
for i in 0..MAX_SC {
self.run_amp_mean[i] = 0.0;
self.run_amp_m2[i] = 0.0;
self.run_phase_mean[i] = 0.0;
self.run_phase_m2[i] = 0.0;
}
}
return unsafe { &EVENTS[..0] };
}
// Compute current amplitude variance / phase variance ratio.
if self.run_count < 4 {
return unsafe { &EVENTS[..0] };
}
let mut ratio_sum = 0.0f32;
let mut valid_sc = 0u32;
let mut max_drift = 0.0f32;
for i in 0..n_sc {
let amp_var = self.run_amp_m2[i] / (self.run_count as f32 - 1.0);
let phase_var = self.run_phase_m2[i] / (self.run_count as f32 - 1.0);
if phase_var > 0.0001 {
let ratio = amp_var / phase_var;
ratio_sum += ratio;
valid_sc += 1;
}
// Check for calibration drift.
let drift = if self.baseline_amp_var[i] > 0.0001 {
fabsf(amp_var - self.baseline_amp_var[i]) / self.baseline_amp_var[i]
} else {
0.0
};
if drift > max_drift {
max_drift = drift;
}
}
if valid_sc < 2 {
return unsafe { &EVENTS[..0] };
}
let mean_ratio = ratio_sum / valid_sc as f32;
// Check for re-calibration need.
if max_drift > RECALIB_DRIFT_THRESH && self.cd_recalib == 0 && ne < 3 {
unsafe { EVENTS[ne] = (EVENT_CALIBRATION_NEEDED, max_drift); }
ne += 1;
self.cd_recalib = COOLDOWN * 5; // Less frequent recalibration alerts.
}
// Metal anomaly detection.
if mean_ratio > METAL_RATIO_THRESH {
self.metal_run = self.metal_run.saturating_add(1);
} else {
self.metal_run = self.metal_run.saturating_sub(1);
}
// Weapon-grade detection (higher threshold).
if mean_ratio > WEAPON_RATIO_THRESH {
self.weapon_run = self.weapon_run.saturating_add(1);
} else {
self.weapon_run = self.weapon_run.saturating_sub(1);
}
// Emit metal anomaly.
if self.metal_run >= METAL_DEBOUNCE && self.cd_metal == 0 && ne < 3 {
unsafe { EVENTS[ne] = (EVENT_METAL_ANOMALY, mean_ratio); }
ne += 1;
self.cd_metal = COOLDOWN;
}
// Emit weapon alert (supersedes metal anomaly in severity).
if self.weapon_run >= WEAPON_DEBOUNCE && self.cd_weapon == 0 && ne < 3 {
unsafe { EVENTS[ne] = (EVENT_WEAPON_ALERT, mean_ratio); }
ne += 1;
self.cd_weapon = COOLDOWN;
}
unsafe { &EVENTS[..ne] }
}
pub fn is_calibrated(&self) -> bool { self.calibrated }
pub fn frame_count(&self) -> u32 { self.frame_count }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init() {
let det = WeaponDetector::new();
assert!(!det.is_calibrated());
assert_eq!(det.frame_count(), 0);
}
#[test]
fn test_calibration_completes() {
let mut det = WeaponDetector::new();
for i in 0..BASELINE_FRAMES {
let p: [f32; 16] = {
let mut arr = [0.0f32; 16];
for j in 0..16 { arr[j] = (i as f32) * 0.01 + (j as f32) * 0.001; }
arr
};
det.process_frame(&p, &[1.0; 16], &[0.01; 16], 0.0, 0);
}
assert!(det.is_calibrated());
}
#[test]
fn test_no_detection_without_presence() {
let mut det = WeaponDetector::new();
// Calibrate.
for i in 0..BASELINE_FRAMES {
let mut p = [0.0f32; 16];
for j in 0..16 { p[j] = (i as f32) * 0.01; }
det.process_frame(&p, &[1.0; 16], &[0.01; 16], 0.0, 0);
}
// Send high-ratio data but with no presence.
for i in 0..50u32 {
let mut p = [0.0f32; 16];
for j in 0..16 { p[j] = 5.0 + (i as f32) * 0.001; }
// High amplitude, low phase change => high ratio, but presence = 0.
let ev = det.process_frame(&p, &[20.0; 16], &[0.01; 16], 0.0, 0);
for &(et, _) in ev {
assert_ne!(et, EVENT_METAL_ANOMALY);
assert_ne!(et, EVENT_WEAPON_ALERT);
}
}
}
#[test]
fn test_metal_anomaly_detection() {
let mut det = WeaponDetector::new();
// Calibrate with moderate signal (some variation for realistic baseline).
for i in 0..BASELINE_FRAMES {
let mut p = [0.0f32; 16];
for j in 0..16 { p[j] = (i as f32) * 0.01 + (j as f32) * 0.001; }
det.process_frame(&p, &[1.0; 16], &[0.01; 16], 0.0, 0);
}
// Simulate person with metal: high amplitude variance, small but nonzero phase variance.
// Metal = specular reflector => amplitude swings wildly between frames,
// while phase changes only slightly (not zero, but much less than amplitude).
let mut found_metal = false;
for i in 0..60u32 {
let mut p = [0.0f32; 16];
// Phase changes slightly per frame (small variance, nonzero).
for j in 0..16 { p[j] = 1.0 + (i as f32) * 0.02 + (j as f32) * 0.01; }
// Amplitude varies hugely between frames (metal strong reflector).
let mut a = [0.0f32; 16];
for j in 0..16 {
a[j] = if (i + j as u32) % 2 == 0 { 15.0 } else { 2.0 };
}
let ev = det.process_frame(&p, &a, &[0.01; 16], 2.0, 1);
for &(et, _) in ev {
if et == EVENT_METAL_ANOMALY {
found_metal = true;
}
}
}
assert!(found_metal, "metal anomaly should be detected");
}
#[test]
fn test_normal_person_no_metal_alert() {
let mut det = WeaponDetector::new();
// Calibrate.
for i in 0..BASELINE_FRAMES {
let mut p = [0.0f32; 16];
for j in 0..16 { p[j] = (i as f32) * 0.01; }
det.process_frame(&p, &[1.0; 16], &[0.01; 16], 0.0, 0);
}
// Normal person: both amplitude and phase vary proportionally.
for i in 0..50u32 {
let mut p = [0.0f32; 16];
let mut a = [0.0f32; 16];
for j in 0..16 {
p[j] = 1.0 + (i as f32) * 0.1 + (j as f32) * 0.05;
a[j] = 1.0 + (i as f32) * 0.1 + (j as f32) * 0.05;
}
let ev = det.process_frame(&p, &a, &[0.01; 16], 1.0, 1);
for &(et, _) in ev {
assert_ne!(et, EVENT_WEAPON_ALERT, "normal person should not trigger weapon alert");
}
}
}
#[test]
fn test_calibration_needed_on_drift() {
let mut det = WeaponDetector::new();
// Calibrate with low, stable amplitudes (small variance baseline).
for i in 0..BASELINE_FRAMES {
let mut p = [0.0f32; 16];
let mut a = [0.0f32; 16];
for j in 0..16 {
p[j] = (i as f32) * 0.01;
// Slight amplitude variation so baseline_amp_var is small but real.
a[j] = 0.5 + (j as f32) * 0.01;
}
det.process_frame(&p, &a, &[0.01; 16], 0.0, 0);
}
// Drastically different environment: huge amplitude swings => large running
// variance that differs vastly from the small calibration baseline.
let mut found_recalib = false;
for i in 0..60u32 {
let mut p = [0.0f32; 16];
let mut a = [0.0f32; 16];
for j in 0..16 {
p[j] = 10.0 + (i as f32) * 0.05;
// Wildly varying amplitudes per frame to build large running variance.
a[j] = if i % 2 == 0 { 50.0 } else { 5.0 };
}
let ev = det.process_frame(&p, &a, &[10.0; 16], 3.0, 1);
for &(et, _) in ev {
if et == EVENT_CALIBRATION_NEEDED {
found_recalib = true;
}
}
}
assert!(found_recalib, "calibration needed should trigger on large drift");
}
#[test]
fn test_too_few_subcarriers() {
let mut det = WeaponDetector::new();
let ev = det.process_frame(&[0.1], &[1.0], &[0.01], 0.0, 0);
assert!(ev.is_empty(), "should return empty with < 2 subcarriers");
}
}