mirror of
https://github.com/ruvnet/RuView
synced 2026-07-27 18:11:43 +00:00
feat(wasm-edge): honest labeling + static-mut soundness for edge skills (ADR-160)
The wasm-edge skill library runs real DSP with 0 stubs / 0 theater; the exposure is an over-confident claim surface on unvalidated skills plus a latent static-mut soundness issue. Make the labels TRUE (do not pretend to validate the capability) and fix the soundness mechanically: - A1 (HIGH): med_seizure/cardiac/respiratory/sleep_apnea/gait -- add mandatory "EXPERIMENTAL / NOT VALIDATED AGAINST CLINICAL DATA / NOT A MEDICAL DEVICE" disclaimers, soften assertive verbs to "flags candidate <X>-like signatures", and gate all 5 behind a NON-default medical-experimental cargo feature so they cannot be silently shipped. DSP kept. - A2 (HIGH): exo_happiness_score/exo_emotion_detect -- delete the uncited "~12% faster" stat, add "speculative, unvalidated affect heuristic; outputs are NOT measurements of emotion" disclaimers, reframe HAPPINESS_SCORE as a gait-energy proxy. Math kept. - A3 (MEDIUM): sec_weapon_detect -- rename EVENT_WEAPON_ALERT -> EVENT_HIGH_METAL_REFLECTIVITY and WEAPON_RATIO_THRESH -> HIGH_REFLECTIVITY_THRESH (a variance ratio measures reflectivity, not weapons). Registry updated. - A4 (MEDIUM): exo_dream_stage/exo_gesture_language -- add experimental disclaimers, promote the Exotic/Research tag into the header. - A5 (MEDIUM, soundness): replace ~61 `static mut EVENTS`/EV/TE/EMPTY per-call scratch buffers (60 modules) with owned per-instance `events` fields returned as `&self.events[..n]`. Public signature unchanged; behavior preserved. Only the two legitimate single-threaded WASM module singletons (lib.rs STATE, ghost_hunter DETECTOR) remain as static mut. Removes the static_mut_refs source. NO-ACTION positives (cited, labels untouched): qnt_* (quantum-/Grover-inspired, disclosed), exo_time_crystal, exo_ghost_hunter, sig_*/lrn_* algorithm-named skills. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -22,6 +22,15 @@ sha2 = { version = "0.10", optional = true, default-features = false }
|
||||
default = ["default-pipeline"]
|
||||
# Enable std for testing on host + RVF builder
|
||||
std = ["sha2/std"]
|
||||
# Experimental medical skills (med_seizure_detect, med_cardiac_arrhythmia,
|
||||
# med_respiratory_distress, med_sleep_apnea, med_gait_analysis).
|
||||
#
|
||||
# ⚠️ NON-DEFAULT BY DESIGN. These modules run real DSP but are NOT validated
|
||||
# against clinical data and are NOT medical devices (ADR-160 §A1). They are
|
||||
# gated behind this feature so they cannot be silently built into a shipping
|
||||
# artifact. Build/test with:
|
||||
# cargo test -p wifi-densepose-wasm-edge --features std,medical-experimental
|
||||
medical-experimental = []
|
||||
# Include the default combined pipeline (gesture+coherence+adversarial) entry points.
|
||||
# Disable this when building standalone module binaries (ghost_hunter, etc.)
|
||||
default-pipeline = []
|
||||
|
||||
@@ -111,6 +111,8 @@ pub struct BehavioralProfiler {
|
||||
obs_cycles: u32,
|
||||
cooldown: u16,
|
||||
anomaly_count: u32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl BehavioralProfiler {
|
||||
@@ -118,6 +120,7 @@ impl BehavioralProfiler {
|
||||
Self {
|
||||
stats: [Welford::new(); N_DIM], obs: ObsWindow::new(),
|
||||
mature: false, frame_count: 0, obs_cycles: 0, cooldown: 0, anomaly_count: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +130,6 @@ impl BehavioralProfiler {
|
||||
self.cooldown = self.cooldown.saturating_sub(1);
|
||||
self.obs.push(present, motion, n_persons);
|
||||
|
||||
static mut EV: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
|
||||
if self.frame_count % (OBS_WIN as u32) == 0 && self.obs.len == OBS_WIN {
|
||||
@@ -139,7 +141,7 @@ impl BehavioralProfiler {
|
||||
if self.obs_cycles >= LEARNING_FRAMES / (OBS_WIN as u32) {
|
||||
self.mature = true;
|
||||
let days = self.frame_count as f32 / (20.0 * 86400.0);
|
||||
unsafe { EV[ne] = (EVENT_PROFILE_MATURITY, days); }
|
||||
self.events[ne] = (EVENT_PROFILE_MATURITY, days);
|
||||
ne += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -159,12 +161,12 @@ impl BehavioralProfiler {
|
||||
if self.cooldown == 0 {
|
||||
if cz > ANOMALY_Z {
|
||||
self.anomaly_count += 1;
|
||||
unsafe { EV[ne] = (EVENT_BEHAVIOR_ANOMALY, cz); } ne += 1;
|
||||
if ne < 4 { unsafe { EV[ne] = (EVENT_PROFILE_DEVIATION, max_d as f32); } ne += 1; }
|
||||
self.events[ne] = (EVENT_BEHAVIOR_ANOMALY, cz); ne += 1;
|
||||
if ne < 4 { self.events[ne] = (EVENT_PROFILE_DEVIATION, max_d as f32); ne += 1; }
|
||||
self.cooldown = COOLDOWN;
|
||||
}
|
||||
if hi_z >= NOVEL_MIN && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_NOVEL_PATTERN, hi_z as f32); } ne += 1;
|
||||
self.events[ne] = (EVENT_NOVEL_PATTERN, hi_z as f32); ne += 1;
|
||||
if self.cooldown == 0 { self.cooldown = COOLDOWN; }
|
||||
}
|
||||
}
|
||||
@@ -173,10 +175,10 @@ impl BehavioralProfiler {
|
||||
|
||||
// Periodic maturity report.
|
||||
if self.mature && self.frame_count % MATURITY_INTERVAL == 0 && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_PROFILE_MATURITY, self.frame_count as f32 / (20.0 * 86400.0)); }
|
||||
self.events[ne] = (EVENT_PROFILE_MATURITY, self.frame_count as f32 / (20.0 * 86400.0));
|
||||
ne += 1;
|
||||
}
|
||||
unsafe { &EV[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn is_mature(&self) -> bool { self.mature }
|
||||
|
||||
@@ -48,6 +48,8 @@ pub struct PromptShield {
|
||||
cd_replay: u16,
|
||||
cd_inject: u16,
|
||||
cd_jam: u16,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl PromptShield {
|
||||
@@ -58,6 +60,7 @@ impl PromptShield {
|
||||
baseline_snr: 0.0, cal_amp: 0.0, cal_var: 0.0, cal_n: 0,
|
||||
calibrated: false, low_snr_run: 0, frame_count: 0,
|
||||
cd_replay: 0, cd_inject: 0, cd_jam: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +73,6 @@ impl PromptShield {
|
||||
self.cd_inject = self.cd_inject.saturating_sub(1);
|
||||
self.cd_jam = self.cd_jam.saturating_sub(1);
|
||||
|
||||
static mut EV: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Frame features: mean phase, mean amp, amp variance.
|
||||
@@ -98,7 +100,7 @@ impl PromptShield {
|
||||
}
|
||||
let h = self.fnv1a(m_ph, m_a, a_var);
|
||||
self.push_hash(h);
|
||||
return unsafe { &EV[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// ── 1. Replay ───────────────────────────────────────────────────
|
||||
@@ -106,7 +108,7 @@ impl PromptShield {
|
||||
let replay = self.has_hash(h);
|
||||
self.push_hash(h);
|
||||
if replay && self.cd_replay == 0 {
|
||||
unsafe { EV[ne] = (EVENT_REPLAY_ATTACK, 1.0); }
|
||||
self.events[ne] = (EVENT_REPLAY_ATTACK, 1.0);
|
||||
ne += 1; self.cd_replay = COOLDOWN;
|
||||
}
|
||||
|
||||
@@ -121,7 +123,7 @@ impl PromptShield {
|
||||
jc as f32 / n as f32
|
||||
} else { 0.0 };
|
||||
if inj_f >= INJECTION_FRAC && self.cd_inject == 0 && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_INJECTION_DETECTED, inj_f); }
|
||||
self.events[ne] = (EVENT_INJECTION_DETECTED, inj_f);
|
||||
ne += 1; self.cd_inject = COOLDOWN;
|
||||
}
|
||||
|
||||
@@ -133,7 +135,7 @@ impl PromptShield {
|
||||
} else { self.low_snr_run = 0; }
|
||||
if self.low_snr_run >= JAMMING_CONSEC && self.cd_jam == 0 && ne < 4 {
|
||||
let r = if cur_snr > 0.0001 { self.baseline_snr / cur_snr } else { 1000.0 };
|
||||
unsafe { EV[ne] = (EVENT_JAMMING_DETECTED, 10.0 * log10f(r)); }
|
||||
self.events[ne] = (EVENT_JAMMING_DETECTED, 10.0 * log10f(r));
|
||||
ne += 1; self.cd_jam = COOLDOWN;
|
||||
}
|
||||
|
||||
@@ -146,12 +148,12 @@ impl PromptShield {
|
||||
let r = cur_snr / self.baseline_snr;
|
||||
if r < 0.5 { s -= (1.0 - r * 2.0).min(0.3); }
|
||||
}
|
||||
unsafe { EV[ne] = (EVENT_SIGNAL_INTEGRITY, if s < 0.0 { 0.0 } else { s }); }
|
||||
self.events[ne] = (EVENT_SIGNAL_INTEGRITY, if s < 0.0 { 0.0 } else { s });
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
for i in 0..n { self.prev_amps[i] = amps[i]; }
|
||||
unsafe { &EV[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
fn fnv1a(&self, ph: f32, amp: f32, var: f32) -> u32 {
|
||||
|
||||
@@ -290,6 +290,8 @@ static KNOWLEDGE_BASE: [Rule; MAX_RULES] = build_knowledge_base();
|
||||
|
||||
/// Psycho-symbolic inference engine.
|
||||
pub struct PsychoSymbolicEngine {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
/// Bitmap of rules that fired in the current frame.
|
||||
fired_rules: u16,
|
||||
/// Previous frame's winning conclusion ID.
|
||||
@@ -307,6 +309,7 @@ pub struct PsychoSymbolicEngine {
|
||||
impl PsychoSymbolicEngine {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
fired_rules: 0,
|
||||
prev_conclusion: 0,
|
||||
contradiction_count: 0,
|
||||
@@ -340,7 +343,6 @@ impl PsychoSymbolicEngine {
|
||||
n_persons: f32,
|
||||
time_bucket: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -372,7 +374,7 @@ impl PsychoSymbolicEngine {
|
||||
|
||||
// Emit RULE_FIRED event (up to budget).
|
||||
if n_events < MAX_EVENTS {
|
||||
unsafe { EVENTS[n_events] = (EVENT_RULE_FIRED, i as f32); }
|
||||
self.events[n_events] = (EVENT_RULE_FIRED, i as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -394,7 +396,7 @@ impl PsychoSymbolicEngine {
|
||||
self.contradiction_count += 1;
|
||||
if n_events < MAX_EVENTS {
|
||||
let encoded = (a as f32) * 100.0 + (b as f32);
|
||||
unsafe { EVENTS[n_events] = (EVENT_CONTRADICTION, encoded); }
|
||||
self.events[n_events] = (EVENT_CONTRADICTION, encoded);
|
||||
n_events += 1;
|
||||
}
|
||||
// Suppress the weaker conclusion.
|
||||
@@ -414,10 +416,10 @@ impl PsychoSymbolicEngine {
|
||||
|
||||
// Emit winning inference.
|
||||
if best_confidence > 0.0 && n_events < MAX_EVENTS {
|
||||
unsafe { EVENTS[n_events] = (EVENT_INFERENCE_RESULT, best_conclusion as f32); }
|
||||
self.events[n_events] = (EVENT_INFERENCE_RESULT, best_conclusion as f32);
|
||||
n_events += 1;
|
||||
if n_events < MAX_EVENTS {
|
||||
unsafe { EVENTS[n_events] = (EVENT_INFERENCE_CONFIDENCE, best_confidence); }
|
||||
self.events[n_events] = (EVENT_INFERENCE_CONFIDENCE, best_confidence);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -426,7 +428,7 @@ impl PsychoSymbolicEngine {
|
||||
self.prev_motion = motion;
|
||||
self.prev_conclusion = best_conclusion;
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Get the bitmap of rules that fired in the last frame.
|
||||
|
||||
@@ -28,6 +28,8 @@ pub const EVENT_HEALING_COMPLETE: i32 = 888;
|
||||
|
||||
/// Self-healing mesh monitor with Stoer-Wagner min-cut analysis.
|
||||
pub struct SelfHealingMesh {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
/// EMA-smoothed quality score per node [0, 1].
|
||||
node_quality: [f32; MAX_NODES],
|
||||
/// Whether each node quality has received its first sample.
|
||||
@@ -49,6 +51,7 @@ pub struct SelfHealingMesh {
|
||||
impl SelfHealingMesh {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
node_quality: [0.0; MAX_NODES],
|
||||
node_init: [false; MAX_NODES],
|
||||
adj: [[0.0; MAX_NODES]; MAX_NODES],
|
||||
@@ -76,7 +79,6 @@ impl SelfHealingMesh {
|
||||
/// per active node (length clamped to 8).
|
||||
/// Returns a slice of (event_id, value) pairs.
|
||||
pub fn process_frame(&mut self, node_qualities: &[f32]) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut ne = 0usize;
|
||||
self.frame_count += 1;
|
||||
|
||||
@@ -84,7 +86,7 @@ impl SelfHealingMesh {
|
||||
self.n_active = n;
|
||||
for i in 0..n { self.update_node_quality(i, node_qualities[i]); }
|
||||
|
||||
if n < 2 { return unsafe { &EVENTS[..0] }; }
|
||||
if n < 2 { return &self.events[..0]; }
|
||||
|
||||
// Build adjacency: edge weight = min(quality_i, quality_j).
|
||||
for i in 0..n {
|
||||
@@ -101,7 +103,7 @@ impl SelfHealingMesh {
|
||||
for i in 0..n { sum += self.node_quality[i]; }
|
||||
let coverage = sum / (n as f32);
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe { EVENTS[ne] = (EVENT_COVERAGE_SCORE, coverage); }
|
||||
self.events[ne] = (EVENT_COVERAGE_SCORE, coverage);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -112,24 +114,24 @@ impl SelfHealingMesh {
|
||||
if !self.healing { self.healing = true; }
|
||||
self.weakest = cut_node;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe { EVENTS[ne] = (EVENT_NODE_DEGRADED, cut_node as f32); }
|
||||
self.events[ne] = (EVENT_NODE_DEGRADED, cut_node as f32);
|
||||
ne += 1;
|
||||
}
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe { EVENTS[ne] = (EVENT_MESH_RECONFIGURE, mincut); }
|
||||
self.events[ne] = (EVENT_MESH_RECONFIGURE, mincut);
|
||||
ne += 1;
|
||||
}
|
||||
} else if self.healing && mincut >= MINCUT_HEALTHY {
|
||||
self.healing = false;
|
||||
self.weakest = NO_NODE;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe { EVENTS[ne] = (EVENT_HEALING_COMPLETE, mincut); }
|
||||
self.events[ne] = (EVENT_HEALING_COMPLETE, mincut);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.prev_mincut = mincut;
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Simplified Stoer-Wagner min-cut for n <= 8 nodes.
|
||||
|
||||
@@ -59,6 +59,8 @@ pub enum DoorState {
|
||||
|
||||
/// Elevator occupancy counter.
|
||||
pub struct ElevatorCounter {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Baseline amplitude per subcarrier (empty cabin).
|
||||
baseline_amp: [f32; MAX_SC],
|
||||
/// Baseline variance per subcarrier.
|
||||
@@ -93,6 +95,7 @@ pub struct ElevatorCounter {
|
||||
impl ElevatorCounter {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
baseline_amp: [0.0; MAX_SC],
|
||||
baseline_var: [0.0; MAX_SC],
|
||||
prev_amp: [0.0; MAX_SC],
|
||||
@@ -268,15 +271,12 @@ impl ElevatorCounter {
|
||||
}
|
||||
|
||||
// ── Build events ────────────────────────────────────────────────
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Door events (immediate).
|
||||
if let Some(evt) = door_event {
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (evt, self.count as f32);
|
||||
}
|
||||
self.events[n_events] = (evt, self.count as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -284,22 +284,18 @@ impl ElevatorCounter {
|
||||
// Periodic count and overload.
|
||||
if self.frame_count % EMIT_INTERVAL == 0 {
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ELEVATOR_COUNT, self.count as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ELEVATOR_COUNT, self.count as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Overload warning.
|
||||
if self.count >= self.overload_thresh && n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_OVERLOAD_WARNING, self.count as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_OVERLOAD_WARNING, self.count as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Get current occupant count estimate.
|
||||
|
||||
@@ -77,6 +77,8 @@ impl HourBin {
|
||||
|
||||
/// Energy audit analyzer.
|
||||
pub struct EnergyAuditor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Weekly histogram: [day][hour].
|
||||
histogram: [[HourBin; HOURS_PER_DAY]; DAYS_PER_WEEK],
|
||||
/// Current simulated hour (0-23). In production, derived from host timestamp.
|
||||
@@ -98,6 +100,7 @@ impl EnergyAuditor {
|
||||
const BIN_INIT: HourBin = HourBin::new();
|
||||
const DAY_INIT: [HourBin; HOURS_PER_DAY] = [BIN_INIT; HOURS_PER_DAY];
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
histogram: [DAY_INIT; DAYS_PER_WEEK],
|
||||
current_hour: 8, // Default start: 8 AM.
|
||||
current_day: 0, // Monday.
|
||||
@@ -161,14 +164,11 @@ impl EnergyAuditor {
|
||||
}
|
||||
|
||||
// Build events.
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// After-hours alert.
|
||||
if self.after_hours_presence >= AFTER_HOURS_ALERT_FRAMES && n_events < 3 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_AFTER_HOURS_ALERT, self.current_hour as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_AFTER_HOURS_ALERT, self.current_hour as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -177,23 +177,19 @@ impl EnergyAuditor {
|
||||
// Emit current hour's occupancy rate.
|
||||
let rate = self.histogram[d][h].occupancy_rate();
|
||||
if n_events < 3 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_SCHEDULE_SUMMARY, rate);
|
||||
}
|
||||
self.events[n_events] = (EVENT_SCHEDULE_SUMMARY, rate);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Emit overall utilization rate.
|
||||
if n_events < 3 {
|
||||
let util = self.utilization_rate();
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_UTILIZATION_RATE, util);
|
||||
}
|
||||
self.events[n_events] = (EVENT_UTILIZATION_RATE, util);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Check if a given hour is after-hours.
|
||||
|
||||
@@ -57,6 +57,8 @@ pub enum ActivityLevel {
|
||||
|
||||
/// HVAC-optimized presence detector.
|
||||
pub struct HvacPresenceDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
state: HvacState,
|
||||
/// Smoothed motion energy (EMA).
|
||||
motion_ema: f32,
|
||||
@@ -73,6 +75,7 @@ pub struct HvacPresenceDetector {
|
||||
impl HvacPresenceDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
state: HvacState::Vacant,
|
||||
motion_ema: 0.0,
|
||||
activity: ActivityLevel::Sedentary,
|
||||
@@ -159,7 +162,6 @@ impl HvacPresenceDetector {
|
||||
}
|
||||
|
||||
// Build output events.
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n = 0usize;
|
||||
|
||||
if self.frame_count % EMIT_INTERVAL == 0 {
|
||||
@@ -168,9 +170,7 @@ impl HvacPresenceDetector {
|
||||
HvacState::Occupied | HvacState::DeparturePending => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_HVAC_OCCUPIED, occupied_val);
|
||||
}
|
||||
self.events[n] = (EVENT_HVAC_OCCUPIED, occupied_val);
|
||||
n += 1;
|
||||
|
||||
// Activity level: 0.0 = sedentary, 1.0 = active, plus raw EMA.
|
||||
@@ -178,9 +178,7 @@ impl HvacPresenceDetector {
|
||||
ActivityLevel::Sedentary => 0.0 + self.motion_ema.min(0.99),
|
||||
ActivityLevel::Active => 1.0,
|
||||
};
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_ACTIVITY_LEVEL, activity_val);
|
||||
}
|
||||
self.events[n] = (EVENT_ACTIVITY_LEVEL, activity_val);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -191,13 +189,11 @@ impl HvacPresenceDetector {
|
||||
{
|
||||
let remaining = DEPARTURE_TIMEOUT.saturating_sub(self.absence_frames);
|
||||
let fraction = remaining as f32 / DEPARTURE_TIMEOUT as f32;
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_DEPARTURE_COUNTDOWN, fraction);
|
||||
}
|
||||
self.events[n] = (EVENT_DEPARTURE_COUNTDOWN, fraction);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Get current HVAC state.
|
||||
|
||||
@@ -76,6 +76,8 @@ struct ZoneLight {
|
||||
|
||||
/// Lighting zone controller.
|
||||
pub struct LightingZoneController {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 8],
|
||||
zones: [ZoneLight; MAX_ZONES],
|
||||
n_zones: usize,
|
||||
/// Calibration accumulators.
|
||||
@@ -99,6 +101,7 @@ impl LightingZoneController {
|
||||
vacant_frames: 0,
|
||||
};
|
||||
Self {
|
||||
events: [(0, 0.0); 8],
|
||||
zones: [ZONE_INIT; MAX_ZONES],
|
||||
n_zones: 0,
|
||||
calib_sum: [0.0; MAX_ZONES],
|
||||
@@ -230,7 +233,6 @@ impl LightingZoneController {
|
||||
}
|
||||
|
||||
// Build output events.
|
||||
static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Emit transitions immediately.
|
||||
@@ -241,9 +243,7 @@ impl LightingZoneController {
|
||||
LightState::Dim => EVENT_LIGHT_DIM,
|
||||
LightState::Off => EVENT_LIGHT_OFF,
|
||||
};
|
||||
unsafe {
|
||||
EVENTS[n_events] = (event_id, z as f32);
|
||||
}
|
||||
self.events[n_events] = (event_id, z as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -259,15 +259,13 @@ impl LightingZoneController {
|
||||
};
|
||||
// Encode zone_id + confidence in value.
|
||||
let val = z as f32 + self.zones[z].score.min(0.99);
|
||||
unsafe {
|
||||
EVENTS[n_events] = (event_id, val);
|
||||
}
|
||||
self.events[n_events] = (event_id, val);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Get the lighting state of a specific zone.
|
||||
|
||||
@@ -54,6 +54,8 @@ pub enum MeetingState {
|
||||
|
||||
/// Meeting room tracker.
|
||||
pub struct MeetingRoomTracker {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
state: MeetingState,
|
||||
/// Frames in current state.
|
||||
state_frames: u32,
|
||||
@@ -76,6 +78,7 @@ pub struct MeetingRoomTracker {
|
||||
impl MeetingRoomTracker {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
state: MeetingState::Empty,
|
||||
state_frames: 0,
|
||||
n_persons: 0,
|
||||
@@ -116,7 +119,6 @@ impl MeetingRoomTracker {
|
||||
self.multi_person_frames += 1;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
let _prev_state = self.state;
|
||||
@@ -146,9 +148,7 @@ impl MeetingRoomTracker {
|
||||
self.meeting_count += 1;
|
||||
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_MEETING_START, self.n_persons as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_MEETING_START, self.n_persons as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
} else if self.state_frames >= PRE_MEETING_TIMEOUT {
|
||||
@@ -175,17 +175,13 @@ impl MeetingRoomTracker {
|
||||
// Emit meeting end with duration.
|
||||
let duration_mins = self.total_meeting_frames as f32 / (20.0 * 60.0);
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_MEETING_END, duration_mins);
|
||||
}
|
||||
self.events[n_events] = (EVENT_MEETING_END, duration_mins);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Emit peak headcount.
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -204,9 +200,7 @@ impl MeetingRoomTracker {
|
||||
self.multi_person_frames = 0;
|
||||
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ROOM_AVAILABLE, 1.0);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ROOM_AVAILABLE, 1.0);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -216,14 +210,12 @@ impl MeetingRoomTracker {
|
||||
// Periodic status emission.
|
||||
if self.frame_count % EMIT_INTERVAL == 0 && self.state == MeetingState::Active {
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_PEAK_HEADCOUNT, self.peak_headcount as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Get current meeting room state.
|
||||
|
||||
@@ -151,6 +151,8 @@ impl PairState {
|
||||
/// group assignment, then computes pairwise cross-correlation to detect
|
||||
/// phase-locked breathing.
|
||||
pub struct BreathingSyncDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Per-person breathing channels (max 4).
|
||||
channels: [BreathingChannel; MAX_PERSONS],
|
||||
/// Pairwise synchronization states (max 6).
|
||||
@@ -170,6 +172,7 @@ pub struct BreathingSyncDetector {
|
||||
impl BreathingSyncDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
channels: [
|
||||
BreathingChannel::new(), BreathingChannel::new(),
|
||||
BreathingChannel::new(), BreathingChannel::new(),
|
||||
@@ -201,7 +204,6 @@ impl BreathingSyncDetector {
|
||||
_breathing_bpm: f32,
|
||||
n_persons: i32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -214,14 +216,12 @@ impl BreathingSyncDetector {
|
||||
if n_pers < 2 {
|
||||
// Reset pair states when fewer than 2 persons.
|
||||
if self.any_synced {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SYNC_LOST, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SYNC_LOST, 1.0);
|
||||
n_ev += 1;
|
||||
self.any_synced = false;
|
||||
self.prev_sync_count = 0;
|
||||
}
|
||||
return unsafe { &EVENTS[..n_ev] };
|
||||
return &self.events[..n_ev];
|
||||
}
|
||||
|
||||
let n_sc = core::cmp::min(phases.len(), MAX_SC);
|
||||
@@ -331,36 +331,28 @@ impl BreathingSyncDetector {
|
||||
|
||||
// Emit events.
|
||||
if self.any_synced && !was_any_synced {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SYNC_DETECTED, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SYNC_DETECTED, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
if was_any_synced && !self.any_synced {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SYNC_LOST, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SYNC_LOST, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
if sync_count != self.prev_sync_count && sync_count > 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SYNC_PAIR_COUNT, sync_count as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SYNC_PAIR_COUNT, sync_count as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.prev_sync_count = sync_count;
|
||||
|
||||
// Emit coherence periodically (every 10 frames).
|
||||
if self.frame_count % 10 == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GROUP_COHERENCE, self.group_coherence);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GROUP_COHERENCE, self.group_coherence);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute normalized cross-correlation between two person channels
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
//! Non-contact sleep stage classification — ADR-041 exotic module.
|
||||
//! Non-contact sleep-stage-like classification — ADR-041 exotic / research module.
|
||||
//!
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED. Quasi-medical sleep-stage
|
||||
//! ⚠️ classification here is a *candidate* heuristic only: it has never been
|
||||
//! ⚠️ compared against polysomnography or any sleep-staging reference standard,
|
||||
//! ⚠️ and its accuracy is unproven (see ADR-160 §A4). NOT a medical device. Do
|
||||
//! ⚠️ NOT use for sleep diagnosis or any clinical decision. (Registry tag:
|
||||
//! ⚠️ Exotic / Research.) The DSP is real; the sleep-stage labels are not
|
||||
//! ⚠️ validated.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
@@ -113,6 +121,8 @@ pub enum SleepStage {
|
||||
|
||||
/// Non-contact sleep stage classifier using WiFi CSI physiological signatures.
|
||||
pub struct DreamStageDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Rolling breathing BPM values.
|
||||
breath_hist: CircularBuffer<BREATH_HIST_LEN>,
|
||||
/// Rolling heart rate BPM values.
|
||||
@@ -152,6 +162,7 @@ pub struct DreamStageDetector {
|
||||
impl DreamStageDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
breath_hist: CircularBuffer::new(),
|
||||
hr_hist: CircularBuffer::new(),
|
||||
phase_buf: CircularBuffer::new(),
|
||||
@@ -192,7 +203,6 @@ impl DreamStageDetector {
|
||||
_variance: f32,
|
||||
presence: i32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -282,33 +292,25 @@ impl DreamStageDetector {
|
||||
};
|
||||
|
||||
// Emit events.
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SLEEP_STAGE, self.current_stage as u8 as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SLEEP_STAGE, self.current_stage as u8 as f32);
|
||||
n_ev += 1;
|
||||
|
||||
// Emit quality periodically (every 20 frames).
|
||||
if self.frame_count % 20 == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SLEEP_QUALITY, efficiency);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SLEEP_QUALITY, efficiency);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_DEEP_SLEEP_RATIO, deep_ratio);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_DEEP_SLEEP_RATIO, deep_ratio);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
// Emit REM episode when in REM or just exited.
|
||||
if rem_ep > 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_REM_EPISODE, rem_ep as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_REM_EPISODE, rem_ep as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Classify the sleep stage from physiological features.
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
//! Affect computing from physiological CSI signatures — ADR-041 exotic module.
|
||||
//! Affect-proxy heuristic from physiological CSI signatures — ADR-041 exotic module.
|
||||
//!
|
||||
//! ⚠️ SPECULATIVE, UNVALIDATED AFFECT HEURISTIC. The outputs of this module
|
||||
//! ⚠️ (`AROUSAL_LEVEL`, `STRESS_INDEX`, `CALM_DETECTED`, `AGITATION_DETECTED`)
|
||||
//! ⚠️ are NOT measurements of emotion. They are threshold-based proxies over
|
||||
//! ⚠️ breathing/motion/heart-rate estimates that have never been correlated
|
||||
//! ⚠️ against self-report, physiological ground truth, or any reference standard
|
||||
//! ⚠️ (see ADR-160 §A2). Do NOT use for affect inference, stress screening, or
|
||||
//! ⚠️ any decision about a person's emotional state. The DSP (rolling statistics
|
||||
//! ⚠️ + weighted scoring) is real; the affect interpretation of its output is not.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
@@ -153,6 +162,8 @@ pub struct EmotionDetector {
|
||||
agitation_detected: bool,
|
||||
/// Total frames processed.
|
||||
frame_count: u32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl EmotionDetector {
|
||||
@@ -171,6 +182,7 @@ impl EmotionDetector {
|
||||
calm_detected: false,
|
||||
agitation_detected: false,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +204,6 @@ impl EmotionDetector {
|
||||
_phase: f32,
|
||||
variance: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -251,31 +262,23 @@ impl EmotionDetector {
|
||||
|| breath_cv > STRESS_BREATH_CV_THRESH);
|
||||
|
||||
// ── Emit events ──
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_AROUSAL_LEVEL, self.arousal);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_AROUSAL_LEVEL, self.arousal);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_STRESS_INDEX, self.stress_index);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_STRESS_INDEX, self.stress_index);
|
||||
n_ev += 1;
|
||||
|
||||
if self.calm_detected {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_CALM_DETECTED, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_CALM_DETECTED, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
if self.agitation_detected {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_AGITATION_DETECTED, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_AGITATION_DETECTED, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute breathing rate score [0, 1].
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
//! Sign language letter recognition from CSI signatures — ADR-041 exotic module.
|
||||
//! Sign-language-letter-like recognition from CSI signatures — ADR-041 exotic / research module.
|
||||
//!
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED. This is a *candidate*
|
||||
//! ⚠️ coarse gesture-cluster classifier, NOT a validated sign-language
|
||||
//! ⚠️ recognizer: it has never been evaluated against a labelled ASL (or any
|
||||
//! ⚠️ sign-language) dataset, accuracy is unproven, and it does not recognize
|
||||
//! ⚠️ true sign language (see ADR-160 §A4). Do NOT rely on its letter labels
|
||||
//! ⚠️ for communication or accessibility. (Registry tag: Exotic / Research.)
|
||||
//! ⚠️ The DSP (feature extraction + template matching) is real; the
|
||||
//! ⚠️ sign-language interpretation is not validated.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
@@ -87,6 +96,8 @@ pub const EVENT_GESTURE_REJECTED: i32 = 623;
|
||||
/// Supports up to 26 letter templates loaded via `set_template()`.
|
||||
/// Uses DTW matching on compact feature sequences.
|
||||
pub struct GestureLanguageDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Template feature sequences: [template_idx][frame][feature].
|
||||
templates: [[[f32; FEAT_DIM]; GESTURE_WIN_LEN]; MAX_TEMPLATES],
|
||||
/// Length of each template (0 = not loaded).
|
||||
@@ -118,6 +129,7 @@ pub struct GestureLanguageDetector {
|
||||
impl GestureLanguageDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
templates: [[[0.0; FEAT_DIM]; GESTURE_WIN_LEN]; MAX_TEMPLATES],
|
||||
template_lens: [0; MAX_TEMPLATES],
|
||||
n_templates: 0,
|
||||
@@ -201,7 +213,6 @@ impl GestureLanguageDetector {
|
||||
motion_energy: f32,
|
||||
presence: i32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -223,29 +234,21 @@ impl GestureLanguageDetector {
|
||||
if self.gesture_fill >= MIN_GESTURE_FILL && self.gesture_active {
|
||||
let (letter, confidence) = self.match_gesture();
|
||||
if letter < MAX_TEMPLATES as u8 && self.since_last_letter >= DEBOUNCE_FRAMES {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_LETTER_RECOGNIZED, letter as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_LETTER_RECOGNIZED, letter as f32);
|
||||
n_ev += 1;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_LETTER_CONFIDENCE, confidence);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_LETTER_CONFIDENCE, confidence);
|
||||
n_ev += 1;
|
||||
self.last_letter = letter;
|
||||
self.last_confidence = confidence;
|
||||
self.since_last_letter = 0;
|
||||
} else {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GESTURE_REJECTED, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GESTURE_REJECTED, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit word boundary.
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_WORD_BOUNDARY, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_WORD_BOUNDARY, 1.0);
|
||||
n_ev += 1;
|
||||
self.word_boundary_emitted = true;
|
||||
self.reset_gesture();
|
||||
@@ -264,7 +267,7 @@ impl GestureLanguageDetector {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Match the current gesture buffer against all loaded templates.
|
||||
|
||||
@@ -123,6 +123,8 @@ pub enum AnomalyClass {
|
||||
|
||||
/// Environmental anomaly detector for empty-room CSI monitoring.
|
||||
pub struct GhostHunterDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Noise floor per subcarrier group (slow EWMA of variance).
|
||||
noise_floor: [Ema; N_GROUPS],
|
||||
/// Anomaly energy buffer per group.
|
||||
@@ -158,6 +160,7 @@ pub struct GhostHunterDetector {
|
||||
impl GhostHunterDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
noise_floor: [
|
||||
Ema::new(NOISE_ALPHA), Ema::new(NOISE_ALPHA),
|
||||
Ema::new(NOISE_ALPHA), Ema::new(NOISE_ALPHA),
|
||||
@@ -203,7 +206,6 @@ impl GhostHunterDetector {
|
||||
presence: i32,
|
||||
motion_energy: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -336,35 +338,27 @@ impl GhostHunterDetector {
|
||||
let norm_energy = if energy > 1.0 { 1.0 } else { energy };
|
||||
|
||||
if anomaly_active {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_ANOMALY_DETECTED, norm_energy);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_ANOMALY_DETECTED, norm_energy);
|
||||
n_ev += 1;
|
||||
|
||||
if self.current_class != AnomalyClass::None {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_ANOMALY_CLASS, self.current_class as u8 as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_ANOMALY_CLASS, self.current_class as u8 as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if self.hidden_presence_score > HIDDEN_PRESENCE_THRESHOLD {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_HIDDEN_PRESENCE, self.hidden_presence_score);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_HIDDEN_PRESENCE, self.hidden_presence_score);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
if self.drift_frames >= DRIFT_MIN_FRAMES {
|
||||
let drift_mag = fabsf(amp_delta) * self.drift_frames as f32;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_ENVIRONMENTAL_DRIFT, drift_mag);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_ENVIRONMENTAL_DRIFT, drift_mag);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Check periodicity in the phase buffer via short autocorrelation.
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
//! Happiness score from WiFi CSI physiological proxies -- ADR-041 exotic module.
|
||||
//! Gait-energy / affect-proxy scoring from WiFi CSI -- ADR-041 exotic module.
|
||||
//!
|
||||
//! ⚠️ SPECULATIVE, UNVALIDATED AFFECT HEURISTIC. The outputs of this module are
|
||||
//! ⚠️ NOT measurements of emotion. `HAPPINESS_SCORE` is a gait-energy / movement
|
||||
//! ⚠️ proxy, not a validated affect measure; it has never been correlated
|
||||
//! ⚠️ against self-report, facial-affect, or any reference standard, and its
|
||||
//! ⚠️ relationship to actual mood is unproven (see ADR-160 §A2). Do NOT use for
|
||||
//! ⚠️ affect inference, screening, or any decision about a person's emotional
|
||||
//! ⚠️ state. The DSP (rolling statistics + weighted scoring) is real; the affect
|
||||
//! ⚠️ interpretation of its output is not.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! Combines six physiological proxies extracted from CSI into a composite
|
||||
//! happiness score [0, 1]:
|
||||
//! Combines six movement/physiology proxies extracted from CSI into a composite
|
||||
//! gait-energy score [0, 1] (labelled `HAPPINESS_SCORE` for the event registry,
|
||||
//! but it is a proxy, not an affect measurement):
|
||||
//!
|
||||
//! 1. **Gait speed** -- Doppler proxy from phase rate-of-change. Happy people
|
||||
//! walk approximately 12% faster than neutral baseline.
|
||||
//! 1. **Gait speed** -- Doppler proxy from phase rate-of-change.
|
||||
//!
|
||||
//! 2. **Stride regularity** -- Variance of step intervals from successive phase
|
||||
//! differences. Regular strides correlate with confidence and positive affect.
|
||||
@@ -31,7 +40,9 @@
|
||||
//!
|
||||
//! # Events (690-694: Exotic / Research)
|
||||
//!
|
||||
//! - `HAPPINESS_SCORE` (690): Composite happiness [0.0 = sad, 0.5 = neutral, 1.0 = happy].
|
||||
//! - `HAPPINESS_SCORE` (690): Composite **gait-energy proxy** [0, 1], NOT a
|
||||
//! validated affect measure. Higher = more energetic/fluid movement, which is
|
||||
//! only speculatively (unvalidated) associated with positive affect.
|
||||
//! - `GAIT_ENERGY` (691): Normalized gait speed/stride score [0, 1].
|
||||
//! - `AFFECT_VALENCE` (692): Emotional valence from breathing + motion [0, 1].
|
||||
//! - `SOCIAL_ENERGY` (693): Group animation/interaction level [0, 1].
|
||||
@@ -97,7 +108,7 @@ const MAX_SC: usize = 32;
|
||||
const EVENT_DECIMATION: u32 = 4;
|
||||
|
||||
/// Baseline gait speed (phase rate-of-change, arbitrary units).
|
||||
/// Happy gait is ~12% above this.
|
||||
/// Used only as a normalization reference for the gait-energy proxy.
|
||||
const BASELINE_GAIT_SPEED: f32 = 0.5;
|
||||
|
||||
/// Maximum expected gait speed for normalization.
|
||||
@@ -184,6 +195,9 @@ pub struct HappinessScoreDetector {
|
||||
|
||||
/// Total frames processed.
|
||||
frame_count: u32,
|
||||
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 5],
|
||||
}
|
||||
|
||||
impl HappinessScoreDetector {
|
||||
@@ -209,6 +223,7 @@ impl HappinessScoreDetector {
|
||||
happiness_vector: [0.0; HAPPINESS_VECTOR_DIM],
|
||||
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 5],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +249,6 @@ impl HappinessScoreDetector {
|
||||
breathing_bpm: f32,
|
||||
heart_rate_bpm: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -341,34 +355,24 @@ impl HappinessScoreDetector {
|
||||
|
||||
// ── Emit events (decimated for ESP32 bandwidth) ──
|
||||
// Always emit happiness score; other events only every Nth frame.
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_HAPPINESS_SCORE, self.happiness);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_HAPPINESS_SCORE, self.happiness);
|
||||
n_ev += 1;
|
||||
|
||||
if self.frame_count % EVENT_DECIMATION == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GAIT_ENERGY, gait_energy);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GAIT_ENERGY, gait_energy);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_AFFECT_VALENCE, affect_valence);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_AFFECT_VALENCE, affect_valence);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_SOCIAL_ENERGY, social_energy);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_SOCIAL_ENERGY, social_energy);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_TRANSIT_DIRECTION, transit);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_TRANSIT_DIRECTION, transit);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Average phase rate-of-change over the rolling window.
|
||||
|
||||
@@ -88,6 +88,8 @@ pub const EVENT_LOCATION_LABEL: i32 = 687;
|
||||
/// Pre-configured with 16 reference points (4 rooms, 12 zones) and a
|
||||
/// linear projection from 8D CSI features to 2D Poincare disk.
|
||||
pub struct HyperbolicEmbedder {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Reference embeddings on the Poincare disk [N_REFS][DIM].
|
||||
references: [[f32; DIM]; N_REFS],
|
||||
/// Linear projection matrix W: [DIM][FEAT_DIM] (2x8).
|
||||
@@ -111,6 +113,7 @@ pub struct HyperbolicEmbedder {
|
||||
impl HyperbolicEmbedder {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
references: Self::default_references(),
|
||||
projection_w: Self::default_projection(),
|
||||
prev_label: 0,
|
||||
@@ -166,7 +169,6 @@ impl HyperbolicEmbedder {
|
||||
///
|
||||
/// Returns events as `(event_id, value)` pairs.
|
||||
pub fn process_frame(&mut self, amplitudes: &[f32]) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
if amplitudes.len() < FEAT_DIM {
|
||||
@@ -250,22 +252,16 @@ impl HyperbolicEmbedder {
|
||||
let level: u8 = if radius < LEVEL_RADIUS_THRESHOLD { 0 } else { 1 };
|
||||
|
||||
// Emit events.
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_HIERARCHY_LEVEL, level as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_HIERARCHY_LEVEL, level as f32);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_HYPERBOLIC_RADIUS, radius);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_HYPERBOLIC_RADIUS, radius);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_LOCATION_LABEL, best_label as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_LOCATION_LABEL, best_label as f32);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Set a reference embedding. `index` must be < N_REFS.
|
||||
|
||||
@@ -99,6 +99,8 @@ pub const EVENT_GESTURE_FERMATA: i32 = 634;
|
||||
/// Extracts tempo, beat position, dynamics, and special gestures from
|
||||
/// WiFi CSI motion patterns.
|
||||
pub struct MusicConductorDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 5],
|
||||
/// Circular buffer of motion energy samples.
|
||||
motion_buf: CircularBuffer<BUF_LEN>,
|
||||
/// Autocorrelation values at lags MIN_LAG..MAX_LAG.
|
||||
@@ -132,6 +134,7 @@ pub struct MusicConductorDetector {
|
||||
impl MusicConductorDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 5],
|
||||
motion_buf: CircularBuffer::new(),
|
||||
autocorr: [0.0; MAX_LAG],
|
||||
tempo_ema: Ema::new(TEMPO_ALPHA),
|
||||
@@ -165,7 +168,6 @@ impl MusicConductorDetector {
|
||||
motion_energy: f32,
|
||||
_variance: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -277,37 +279,27 @@ impl MusicConductorDetector {
|
||||
|
||||
// ── Emit events ──
|
||||
if self.tempo_ema.is_initialized() {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_CONDUCTOR_BPM, self.tempo_ema.value);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_CONDUCTOR_BPM, self.tempo_ema.value);
|
||||
n_ev += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_BEAT_POSITION, beat_position as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_BEAT_POSITION, beat_position as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_DYNAMIC_LEVEL, dynamic_level);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_DYNAMIC_LEVEL, dynamic_level);
|
||||
n_ev += 1;
|
||||
|
||||
if self.cutoff_detected {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GESTURE_CUTOFF, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GESTURE_CUTOFF, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
if self.fermata_active {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GESTURE_FERMATA, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GESTURE_FERMATA, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute buffer mean and variance (single-pass).
|
||||
|
||||
@@ -95,6 +95,8 @@ pub const EVENT_WATERING_EVENT: i32 = 643;
|
||||
/// and phase to detect growth drift, circadian oscillation, wilting,
|
||||
/// and watering events.
|
||||
pub struct PlantGrowthDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Slow EWMA of amplitude per subcarrier group.
|
||||
amp_baseline: [Ema; N_GROUPS],
|
||||
/// Fast EWMA of amplitude per subcarrier group.
|
||||
@@ -124,6 +126,7 @@ pub struct PlantGrowthDetector {
|
||||
impl PlantGrowthDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
amp_baseline: [
|
||||
Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA),
|
||||
Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA),
|
||||
@@ -174,7 +177,6 @@ impl PlantGrowthDetector {
|
||||
variance: &[f32],
|
||||
presence: i32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -264,9 +266,7 @@ impl PlantGrowthDetector {
|
||||
self.drift_interval_count = 0;
|
||||
|
||||
if fabsf(avg_drift) > GROWTH_THRESHOLD {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GROWTH_RATE, avg_drift);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GROWTH_RATE, avg_drift);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
@@ -288,9 +288,7 @@ impl PlantGrowthDetector {
|
||||
if avg_osc > CIRCADIAN_MIN_MAGNITUDE {
|
||||
// Normalize to [0, 1] range (cap at 1.0).
|
||||
let normalized = if avg_osc > 1.0 { 1.0 } else { avg_osc };
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_CIRCADIAN_PHASE, normalized);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_CIRCADIAN_PHASE, normalized);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
@@ -315,9 +313,7 @@ impl PlantGrowthDetector {
|
||||
}
|
||||
// Need majority of groups to agree.
|
||||
if amp_rise_count >= (N_GROUPS / 2) as u8 && var_drop_count >= 2 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_WILT_DETECTED, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_WILT_DETECTED, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
@@ -333,14 +329,12 @@ impl PlantGrowthDetector {
|
||||
}
|
||||
}
|
||||
if drop_count >= (N_GROUPS / 2) as u8 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_WATERING_EVENT, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_WATERING_EVENT, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Get the number of empty-room frames accumulated.
|
||||
|
||||
@@ -99,6 +99,8 @@ pub enum RainIntensity {
|
||||
|
||||
/// Detects rain from broadband CSI phase variance perturbations.
|
||||
pub struct RainDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Baseline variance per subcarrier group (slow EWMA).
|
||||
baseline_var: [Ema; N_GROUPS],
|
||||
/// Short-term variance per subcarrier group (fast EWMA).
|
||||
@@ -122,6 +124,7 @@ pub struct RainDetector {
|
||||
impl RainDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
baseline_var: [
|
||||
Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA),
|
||||
Ema::new(BASELINE_ALPHA), Ema::new(BASELINE_ALPHA),
|
||||
@@ -159,7 +162,6 @@ impl RainDetector {
|
||||
amplitudes: &[f32],
|
||||
presence: i32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.frame_count += 1;
|
||||
@@ -250,9 +252,7 @@ impl RainDetector {
|
||||
// Onset: was not raining, now have enough consecutive rain frames.
|
||||
if !self.raining && self.rain_frames >= ONSET_FRAMES {
|
||||
self.raining = true;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_RAIN_ONSET, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_RAIN_ONSET, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
@@ -260,9 +260,7 @@ impl RainDetector {
|
||||
if was_raining && self.quiet_frames >= CESSATION_FRAMES {
|
||||
self.raining = false;
|
||||
self.intensity = RainIntensity::None;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_RAIN_CESSATION, 1.0);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_RAIN_CESSATION, 1.0);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
@@ -277,13 +275,11 @@ impl RainDetector {
|
||||
RainIntensity::Heavy
|
||||
};
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_RAIN_INTENSITY, self.intensity as u8 as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_RAIN_INTENSITY, self.intensity as u8 as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Whether rain is currently detected.
|
||||
|
||||
@@ -74,6 +74,8 @@ pub const EVENT_COORDINATION_INDEX: i32 = 682;
|
||||
/// Samples `motion_energy` into a circular buffer and runs autocorrelation
|
||||
/// to detect period doubling and multi-person temporal coordination.
|
||||
pub struct TimeCrystalDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Circular buffer of motion energy samples.
|
||||
motion_buf: CircularBuffer<BUF_LEN>,
|
||||
/// Autocorrelation values at lags 1..MAX_LAG.
|
||||
@@ -101,6 +103,7 @@ pub struct TimeCrystalDetector {
|
||||
impl TimeCrystalDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
motion_buf: CircularBuffer::new(),
|
||||
autocorr: [0.0; MAX_LAG],
|
||||
last_multiplier: 0,
|
||||
@@ -119,7 +122,6 @@ impl TimeCrystalDetector {
|
||||
///
|
||||
/// Returns events as `(event_id, value)` pairs in a static buffer.
|
||||
pub fn process_frame(&mut self, motion_energy: f32) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
// Push sample into circular buffer.
|
||||
@@ -216,25 +218,19 @@ impl TimeCrystalDetector {
|
||||
|
||||
// Emit events.
|
||||
if detected_multiplier > 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_CRYSTAL_DETECTED, detected_multiplier as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_CRYSTAL_DETECTED, detected_multiplier as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_CRYSTAL_STABILITY, self.stability_ema.value);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_CRYSTAL_STABILITY, self.stability_ema.value);
|
||||
n_ev += 1;
|
||||
|
||||
if coordination > 0 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_COORDINATION_INDEX, coordination as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_COORDINATION_INDEX, coordination as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute mean and variance of the circular buffer contents.
|
||||
|
||||
@@ -41,6 +41,8 @@ pub const EVENT_COMPLIANCE_REPORT: i32 = 523;
|
||||
|
||||
/// Clean room monitor.
|
||||
pub struct CleanRoomMonitor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Maximum allowed occupancy.
|
||||
max_occupancy: u8,
|
||||
/// Current smoothed person count.
|
||||
@@ -70,6 +72,7 @@ pub struct CleanRoomMonitor {
|
||||
impl CleanRoomMonitor {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
max_occupancy: DEFAULT_MAX_OCCUPANCY,
|
||||
current_count: 0,
|
||||
prev_count: 0,
|
||||
@@ -88,6 +91,7 @@ impl CleanRoomMonitor {
|
||||
/// Create with custom maximum occupancy.
|
||||
pub const fn with_max_occupancy(max: u8) -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
max_occupancy: max,
|
||||
current_count: 0,
|
||||
prev_count: 0,
|
||||
@@ -146,12 +150,11 @@ impl CleanRoomMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// --- Step 1: Emit count changes ---
|
||||
if count != self.prev_count && n_events < 4 {
|
||||
unsafe { EVENTS[n_events] = (EVENT_OCCUPANCY_COUNT, count as f32); }
|
||||
self.events[n_events] = (EVENT_OCCUPANCY_COUNT, count as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ impl CleanRoomMonitor {
|
||||
self.violation_cooldown = VIOLATION_COOLDOWN;
|
||||
// Value encodes: count * 10 + max_allowed.
|
||||
let val = count as f32;
|
||||
unsafe { EVENTS[n_events] = (EVENT_OCCUPANCY_VIOLATION, val); }
|
||||
self.events[n_events] = (EVENT_OCCUPANCY_VIOLATION, val);
|
||||
n_events += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -182,7 +185,7 @@ impl CleanRoomMonitor {
|
||||
{
|
||||
self.total_turbulent += 1;
|
||||
self.turbulent_cooldown = TURBULENT_COOLDOWN;
|
||||
unsafe { EVENTS[n_events] = (EVENT_TURBULENT_MOTION, motion_energy); }
|
||||
self.events[n_events] = (EVENT_TURBULENT_MOTION, motion_energy);
|
||||
n_events += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -196,11 +199,11 @@ impl CleanRoomMonitor {
|
||||
} else {
|
||||
100.0
|
||||
};
|
||||
unsafe { EVENTS[n_events] = (EVENT_COMPLIANCE_REPORT, compliance_pct); }
|
||||
self.events[n_events] = (EVENT_COMPLIANCE_REPORT, compliance_pct);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Current occupancy count.
|
||||
|
||||
@@ -55,6 +55,8 @@ pub enum WorkerState {
|
||||
|
||||
/// Confined space monitor.
|
||||
pub struct ConfinedSpaceMonitor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Current worker state.
|
||||
state: WorkerState,
|
||||
/// Presence debounce counters.
|
||||
@@ -79,6 +81,7 @@ pub struct ConfinedSpaceMonitor {
|
||||
impl ConfinedSpaceMonitor {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
state: WorkerState::Empty,
|
||||
present_count: 0,
|
||||
absent_count: 0,
|
||||
@@ -110,7 +113,6 @@ impl ConfinedSpaceMonitor {
|
||||
) -> &[(i32, f32)] {
|
||||
self.frame_count += 1;
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// --- Step 1: Debounced presence detection ---
|
||||
@@ -141,7 +143,7 @@ impl ConfinedSpaceMonitor {
|
||||
self.extraction_alerted = false;
|
||||
self.immobile_alerted = false;
|
||||
if n_events < 4 {
|
||||
unsafe { EVENTS[n_events] = (EVENT_WORKER_ENTRY, 1.0); }
|
||||
self.events[n_events] = (EVENT_WORKER_ENTRY, 1.0);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -150,7 +152,7 @@ impl ConfinedSpaceMonitor {
|
||||
if !self.worker_inside && was_inside {
|
||||
self.state = WorkerState::Empty;
|
||||
if n_events < 4 {
|
||||
unsafe { EVENTS[n_events] = (EVENT_WORKER_EXIT, 1.0); }
|
||||
self.events[n_events] = (EVENT_WORKER_EXIT, 1.0);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +171,7 @@ impl ConfinedSpaceMonitor {
|
||||
|
||||
// Periodic breathing confirmation.
|
||||
if self.frame_count % BREATHING_REPORT_INTERVAL == 0 && n_events < 4 {
|
||||
unsafe { EVENTS[n_events] = (EVENT_BREATHING_OK, breathing_bpm); }
|
||||
self.events[n_events] = (EVENT_BREATHING_OK, breathing_bpm);
|
||||
n_events += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -197,7 +199,7 @@ impl ConfinedSpaceMonitor {
|
||||
self.state = WorkerState::BreathingCeased;
|
||||
self.extraction_alerted = true;
|
||||
let seconds = self.no_breathing_frames as f32 / 20.0;
|
||||
unsafe { EVENTS[n_events] = (EVENT_EXTRACTION_ALERT, seconds); }
|
||||
self.events[n_events] = (EVENT_EXTRACTION_ALERT, seconds);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -209,12 +211,12 @@ impl ConfinedSpaceMonitor {
|
||||
self.state = WorkerState::Immobile;
|
||||
self.immobile_alerted = true;
|
||||
let seconds = self.no_motion_frames as f32 / 20.0;
|
||||
unsafe { EVENTS[n_events] = (EVENT_IMMOBILE_ALERT, seconds); }
|
||||
self.events[n_events] = (EVENT_IMMOBILE_ALERT, seconds);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Current worker state.
|
||||
|
||||
@@ -59,6 +59,8 @@ pub const EVENT_HUMAN_NEAR_VEHICLE: i32 = 502;
|
||||
|
||||
/// Forklift proximity detector.
|
||||
pub struct ForkliftProximityDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Per-subcarrier baseline amplitude (calibrated).
|
||||
baseline_amp: [f32; MAX_SC],
|
||||
/// Phase history ring buffer for frequency analysis.
|
||||
@@ -83,6 +85,7 @@ pub struct ForkliftProximityDetector {
|
||||
impl ForkliftProximityDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
baseline_amp: [0.0; MAX_SC],
|
||||
phase_history: [[0.0; MAX_SC]; PHASE_HISTORY],
|
||||
phase_hist_idx: 0,
|
||||
@@ -139,7 +142,6 @@ impl ForkliftProximityDetector {
|
||||
self.phase_hist_len += 1;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Calibration phase: 100 frames (~5 seconds).
|
||||
@@ -158,7 +160,7 @@ impl ForkliftProximityDetector {
|
||||
}
|
||||
self.calibrated = true;
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// --- Step 1: Detect forklift/AGV signature ---
|
||||
@@ -182,9 +184,7 @@ impl ForkliftProximityDetector {
|
||||
|
||||
// Emit vehicle detected on transition.
|
||||
if self.vehicle_present && !was_vehicle && n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_VEHICLE_DETECTED, amp_ratio);
|
||||
}
|
||||
self.events[n_events] = (EVENT_VEHICLE_DETECTED, amp_ratio);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -197,9 +197,7 @@ impl ForkliftProximityDetector {
|
||||
|
||||
// Emit human-near-vehicle event on transition (debounce threshold reached).
|
||||
if self.proximity_debounce == PROXIMITY_DEBOUNCE && n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_HUMAN_NEAR_VEHICLE, motion_energy);
|
||||
}
|
||||
self.events[n_events] = (EVENT_HUMAN_NEAR_VEHICLE, motion_energy);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -215,9 +213,7 @@ impl ForkliftProximityDetector {
|
||||
} else {
|
||||
2.0 // caution
|
||||
};
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_PROXIMITY_WARNING, dist_cat);
|
||||
}
|
||||
self.events[n_events] = (EVENT_PROXIMITY_WARNING, dist_cat);
|
||||
n_events += 1;
|
||||
self.cooldown = ALERT_COOLDOWN;
|
||||
}
|
||||
@@ -225,7 +221,7 @@ impl ForkliftProximityDetector {
|
||||
self.proximity_debounce = 0;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Compute mean amplitude ratio vs baseline across subcarriers.
|
||||
|
||||
@@ -72,6 +72,8 @@ impl Species {
|
||||
|
||||
/// Livestock monitor.
|
||||
pub struct LivestockMonitor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Configured species.
|
||||
species: Species,
|
||||
/// Whether animal is currently detected (debounced).
|
||||
@@ -97,6 +99,7 @@ pub struct LivestockMonitor {
|
||||
impl LivestockMonitor {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
species: Species::Cattle,
|
||||
animal_present: false,
|
||||
presence_frames: 0,
|
||||
@@ -113,6 +116,7 @@ impl LivestockMonitor {
|
||||
/// Create with a specific species.
|
||||
pub const fn with_species(species: Species) -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
species,
|
||||
animal_present: false,
|
||||
presence_frames: 0,
|
||||
@@ -148,7 +152,6 @@ impl LivestockMonitor {
|
||||
self.escape_cooldown -= 1;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
let raw_present = presence > 0 || motion_energy > MIN_MOTION_ACTIVE;
|
||||
@@ -177,7 +180,7 @@ impl LivestockMonitor {
|
||||
{
|
||||
self.escape_cooldown = ESCAPE_COOLDOWN;
|
||||
let minutes_present = self.presence_frames as f32 / (20.0 * 60.0);
|
||||
unsafe { EVENTS[n_events] = (EVENT_ESCAPE_ALERT, minutes_present); }
|
||||
self.events[n_events] = (EVENT_ESCAPE_ALERT, minutes_present);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -190,7 +193,7 @@ impl LivestockMonitor {
|
||||
&& self.frame_count % PRESENCE_REPORT_INTERVAL == 0
|
||||
&& n_events < 4
|
||||
{
|
||||
unsafe { EVENTS[n_events] = (EVENT_ANIMAL_PRESENT, breathing_bpm); }
|
||||
self.events[n_events] = (EVENT_ANIMAL_PRESENT, breathing_bpm);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
@@ -209,7 +212,7 @@ impl LivestockMonitor {
|
||||
{
|
||||
self.stillness_alerted = true;
|
||||
let minutes_still = self.still_frames as f32 / (20.0 * 60.0);
|
||||
unsafe { EVENTS[n_events] = (EVENT_ABNORMAL_STILLNESS, minutes_still); }
|
||||
self.events[n_events] = (EVENT_ABNORMAL_STILLNESS, minutes_still);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -226,7 +229,7 @@ impl LivestockMonitor {
|
||||
if is_labored {
|
||||
self.labored_debounce = self.labored_debounce.saturating_add(1);
|
||||
if self.labored_debounce >= LABORED_DEBOUNCE && n_events < 4 {
|
||||
unsafe { EVENTS[n_events] = (EVENT_LABORED_BREATHING, breathing_bpm); }
|
||||
self.events[n_events] = (EVENT_LABORED_BREATHING, breathing_bpm);
|
||||
n_events += 1;
|
||||
self.labored_debounce = 0; // Reset to allow repeated alerts.
|
||||
}
|
||||
@@ -235,7 +238,7 @@ impl LivestockMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Whether an animal is currently detected.
|
||||
|
||||
@@ -72,6 +72,8 @@ pub const EVENT_VIBRATION_SPECTRUM: i32 = 543;
|
||||
|
||||
/// Structural vibration monitor.
|
||||
pub struct StructuralVibrationMonitor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Phase history ring buffer [time][subcarrier].
|
||||
phase_history: [[f32; MAX_SC]; PHASE_HISTORY_LEN],
|
||||
hist_idx: usize,
|
||||
@@ -104,6 +106,7 @@ pub struct StructuralVibrationMonitor {
|
||||
impl StructuralVibrationMonitor {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
phase_history: [[0.0; MAX_SC]; PHASE_HISTORY_LEN],
|
||||
hist_idx: 0,
|
||||
hist_len: 0,
|
||||
@@ -162,7 +165,6 @@ impl StructuralVibrationMonitor {
|
||||
self.hist_len += 1;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// --- Calibration: establish baseline when space is empty ---
|
||||
@@ -180,7 +182,7 @@ impl StructuralVibrationMonitor {
|
||||
self.baseline_set = true;
|
||||
}
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Only analyze when unoccupied (human presence masks structural signals).
|
||||
@@ -191,7 +193,7 @@ impl StructuralVibrationMonitor {
|
||||
self.drift_direction[i] = 0;
|
||||
self.drift_accumulator[i] = 0.0;
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// --- Step 1: Compute phase deviation RMS ---
|
||||
@@ -209,7 +211,7 @@ impl StructuralVibrationMonitor {
|
||||
&& n_events < 4
|
||||
{
|
||||
self.seismic_cooldown = SEISMIC_COOLDOWN;
|
||||
unsafe { EVENTS[n_events] = (EVENT_SEISMIC_DETECTED, rms); }
|
||||
self.events[n_events] = (EVENT_SEISMIC_DETECTED, rms);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -235,7 +237,7 @@ impl StructuralVibrationMonitor {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
unsafe { EVENTS[n_events] = (EVENT_MECHANICAL_RESONANCE, freq); }
|
||||
self.events[n_events] = (EVENT_MECHANICAL_RESONANCE, freq);
|
||||
n_events += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -253,7 +255,7 @@ impl StructuralVibrationMonitor {
|
||||
if fabsf(avg_drift) > DRIFT_RATE_THRESH {
|
||||
self.drift_cooldown = DRIFT_COOLDOWN;
|
||||
// Value is drift rate in rad/second.
|
||||
unsafe { EVENTS[n_events] = (EVENT_STRUCTURAL_DRIFT, avg_drift * 20.0); }
|
||||
self.events[n_events] = (EVENT_STRUCTURAL_DRIFT, avg_drift * 20.0);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -263,11 +265,11 @@ impl StructuralVibrationMonitor {
|
||||
&& self.hist_len >= MAX_LAGS + 1
|
||||
&& n_events < 4
|
||||
{
|
||||
unsafe { EVENTS[n_events] = (EVENT_VIBRATION_SPECTRUM, rms); }
|
||||
self.events[n_events] = (EVENT_VIBRATION_SPECTRUM, rms);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Compute RMS phase deviation from baseline.
|
||||
|
||||
@@ -57,6 +57,8 @@ pub enum DetectorState {
|
||||
|
||||
/// Intrusion detector.
|
||||
pub struct IntrusionDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Per-subcarrier baseline amplitude.
|
||||
baseline_amp: [f32; MAX_SC],
|
||||
/// Per-subcarrier baseline variance.
|
||||
@@ -86,6 +88,7 @@ pub struct IntrusionDetector {
|
||||
impl IntrusionDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
baseline_amp: [0.0; MAX_SC],
|
||||
baseline_var: [0.0; MAX_SC],
|
||||
prev_phases: [0.0; MAX_SC],
|
||||
@@ -119,7 +122,6 @@ impl IntrusionDetector {
|
||||
self.cooldown -= 1;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
match self.state {
|
||||
@@ -165,9 +167,7 @@ impl IntrusionDetector {
|
||||
if self.quiet_frames >= ARM_FRAMES {
|
||||
self.state = DetectorState::Armed;
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_INTRUSION_ARMED, 1.0);
|
||||
}
|
||||
self.events[n_events] = (EVENT_INTRUSION_ARMED, 1.0);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -190,18 +190,14 @@ impl IntrusionDetector {
|
||||
self.cooldown = ALERT_COOLDOWN;
|
||||
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_INTRUSION_ALERT, disturbance);
|
||||
}
|
||||
self.events[n_events] = (EVENT_INTRUSION_ALERT, disturbance);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Find the most disturbed zone.
|
||||
let zone = self.find_disturbed_zone(amplitudes, n_sc);
|
||||
if n_events < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_INTRUSION_ZONE, zone as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_INTRUSION_ZONE, zone as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -235,7 +231,7 @@ impl IntrusionDetector {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Compute overall disturbance score.
|
||||
|
||||
@@ -46,10 +46,20 @@ pub mod vital_trend;
|
||||
pub mod intrusion;
|
||||
|
||||
// ── Category 1: Medical & Health (ADR-041, event IDs 100-199) ───────────────
|
||||
//
|
||||
// ⚠️ EXPERIMENTAL — NOT clinically validated, NOT medical devices (ADR-160 §A1).
|
||||
// Gated behind the non-default `medical-experimental` feature so they cannot be
|
||||
// silently built into a shipping artifact. The DSP is real; the clinical claim
|
||||
// surface is not. See each module's header disclaimer.
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub mod med_sleep_apnea;
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub mod med_cardiac_arrhythmia;
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub mod med_respiratory_distress;
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub mod med_gait_analysis;
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub mod med_seizure_detect;
|
||||
|
||||
// ── Category 2: Security & Safety (ADR-041, event IDs 200-299) ──────────────
|
||||
@@ -228,9 +238,11 @@ pub mod event_types {
|
||||
pub const DEPARTURE_DETECTED: i32 = 212;
|
||||
pub const SEC_ZONE_TRANSITION: i32 = 213;
|
||||
|
||||
// sec_weapon_detect (220-222)
|
||||
// sec_weapon_detect (220-222) — ADR-160 §A3: honest physical-quantity names.
|
||||
// `WEAPON_ALERT` was renamed to `HIGH_METAL_REFLECTIVITY`: a variance ratio
|
||||
// measures RF reflectivity, not weapon-grade discrimination.
|
||||
pub const METAL_ANOMALY: i32 = 220;
|
||||
pub const WEAPON_ALERT: i32 = 221;
|
||||
pub const HIGH_METAL_REFLECTIVITY: i32 = 221;
|
||||
pub const CALIBRATION_NEEDED: i32 = 222;
|
||||
|
||||
// sec_tailgating (230-232)
|
||||
|
||||
@@ -71,6 +71,8 @@ type StateVec = [f32; STATE_DIM];
|
||||
|
||||
/// Attractor-based anomaly detector.
|
||||
pub struct AttractorDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Circular trajectory buffer.
|
||||
trajectory: [StateVec; TRAJ_LEN],
|
||||
/// Write index into trajectory buffer.
|
||||
@@ -108,6 +110,7 @@ pub struct AttractorDetector {
|
||||
impl AttractorDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
trajectory: [[0.0; STATE_DIM]; TRAJ_LEN],
|
||||
traj_idx: 0,
|
||||
traj_len: 0,
|
||||
@@ -137,7 +140,6 @@ impl AttractorDetector {
|
||||
amplitudes: &[f32],
|
||||
motion_energy: f32,
|
||||
) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
let n_sc = phases.len().min(amplitudes.len());
|
||||
@@ -200,16 +202,14 @@ impl AttractorDetector {
|
||||
self.radius = 0.01;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_LEARNING_COMPLETE, 1.0);
|
||||
n_ev += 1;
|
||||
EVENTS[n_ev] = (EVENT_ATTRACTOR_TYPE, self.attractor_type as u8 as f32);
|
||||
n_ev += 1;
|
||||
EVENTS[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_LEARNING_COMPLETE, 1.0);
|
||||
n_ev += 1;
|
||||
self.events[n_ev] = (EVENT_ATTRACTOR_TYPE, self.attractor_type as u8 as f32);
|
||||
n_ev += 1;
|
||||
self.events[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda);
|
||||
n_ev += 1;
|
||||
|
||||
return unsafe { &EVENTS[..n_ev] };
|
||||
return &self.events[..n_ev];
|
||||
}
|
||||
|
||||
return &[];
|
||||
@@ -221,10 +221,8 @@ impl AttractorDetector {
|
||||
|
||||
if dist > departure_threshold && self.cooldown == 0 {
|
||||
self.cooldown = DEPARTURE_COOLDOWN;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_BASIN_DEPARTURE, dist / self.radius);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_BASIN_DEPARTURE, dist / self.radius);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
// ── Periodic attractor update (every 200 frames) ────────────────
|
||||
@@ -234,16 +232,14 @@ impl AttractorDetector {
|
||||
|
||||
if new_type != self.attractor_type && n_ev < 3 {
|
||||
self.attractor_type = new_type;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_ATTRACTOR_TYPE, new_type as u8 as f32);
|
||||
n_ev += 1;
|
||||
EVENTS[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_ATTRACTOR_TYPE, new_type as u8 as f32);
|
||||
n_ev += 1;
|
||||
self.events[n_ev] = (EVENT_LYAPUNOV_EXPONENT, lambda);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute the current largest Lyapunov exponent estimate.
|
||||
|
||||
@@ -85,6 +85,8 @@ impl Template {
|
||||
|
||||
/// User-teachable gesture learner and recognizer.
|
||||
pub struct GestureLearner {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
// ── Stored templates ─────────────────────────────────────────────────
|
||||
templates: [Template; MAX_TEMPLATES],
|
||||
template_count: usize,
|
||||
@@ -117,6 +119,7 @@ pub struct GestureLearner {
|
||||
impl GestureLearner {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
templates: [Template::empty(); MAX_TEMPLATES],
|
||||
template_count: 0,
|
||||
learn_phase: LearnPhase::Idle,
|
||||
@@ -143,7 +146,6 @@ impl GestureLearner {
|
||||
///
|
||||
/// Returns events as `(event_id, value)` pairs in a static buffer.
|
||||
pub fn process_frame(&mut self, phases: &[f32], motion_energy: f32) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
if phases.is_empty() {
|
||||
@@ -228,12 +230,10 @@ impl GestureLearner {
|
||||
// Check if all 3 rehearsals are mutually similar.
|
||||
if self.rehearsals_are_similar() {
|
||||
if let Some(id) = self.commit_template() {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GESTURE_LEARNED, id as f32);
|
||||
n_ev += 1;
|
||||
EVENTS[n_ev] = (EVENT_TEMPLATE_COUNT, self.template_count as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_GESTURE_LEARNED, id as f32);
|
||||
n_ev += 1;
|
||||
self.events[n_ev] = (EVENT_TEMPLATE_COUNT, self.template_count as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
// Reset learning state regardless.
|
||||
@@ -284,18 +284,16 @@ impl GestureLearner {
|
||||
|
||||
if let Some(id) = best_id {
|
||||
self.cooldown = MATCH_COOLDOWN;
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_GESTURE_MATCHED, id as f32);
|
||||
self.events[n_ev] = (EVENT_GESTURE_MATCHED, id as f32);
|
||||
n_ev += 1;
|
||||
if n_ev < 4 {
|
||||
self.events[n_ev] = (EVENT_MATCH_DISTANCE, best_dist);
|
||||
n_ev += 1;
|
||||
if n_ev < 4 {
|
||||
EVENTS[n_ev] = (EVENT_MATCH_DISTANCE, best_dist);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Check if all rehearsals are pairwise similar (DTW distance < threshold).
|
||||
|
||||
@@ -99,6 +99,8 @@ pub const EVENT_FORGETTING_RISK: i32 = 748;
|
||||
|
||||
/// Elastic Weight Consolidation lifelong on-device learner.
|
||||
pub struct EwcLifelong {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Current learnable parameters [N_PARAMS] (flattened [N_OUTPUT][N_INPUT]).
|
||||
params: [f32; N_PARAMS],
|
||||
/// Fisher Information diagonal [N_PARAMS].
|
||||
@@ -128,6 +130,7 @@ pub struct EwcLifelong {
|
||||
impl EwcLifelong {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
params: Self::default_params(),
|
||||
fisher: [0.0; N_PARAMS],
|
||||
theta_star: [0.0; N_PARAMS],
|
||||
@@ -169,7 +172,6 @@ impl EwcLifelong {
|
||||
///
|
||||
/// Returns events as `(event_id, value)` pairs.
|
||||
pub fn process_frame(&mut self, features: &[f32], target_zone: i32) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
if features.len() < N_INPUT {
|
||||
@@ -217,17 +219,13 @@ impl EwcLifelong {
|
||||
&& self.task_count < MAX_TASKS
|
||||
{
|
||||
self.commit_task();
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_NEW_TASK_LEARNED, self.task_count as f32);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_NEW_TASK_LEARNED, self.task_count as f32);
|
||||
n_ev += 1;
|
||||
|
||||
// Emit mean Fisher value.
|
||||
let mean_fisher = self.mean_fisher();
|
||||
if n_ev < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_FISHER_UPDATE, mean_fisher);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_FISHER_UPDATE, mean_fisher);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
@@ -235,9 +233,7 @@ impl EwcLifelong {
|
||||
// Periodic reporting.
|
||||
if self.frame_count % REPORT_INTERVAL == 0 {
|
||||
if n_ev < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_KNOWLEDGE_RETAINED, ewc_penalty);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_KNOWLEDGE_RETAINED, ewc_penalty);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
@@ -248,15 +244,13 @@ impl EwcLifelong {
|
||||
0.0
|
||||
};
|
||||
if n_ev < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_FORGETTING_RISK, risk);
|
||||
}
|
||||
self.events[n_ev] = (EVENT_FORGETTING_RISK, risk);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Forward pass: linear classifier `output = params * features`.
|
||||
|
||||
@@ -85,6 +85,8 @@ enum OptPhase {
|
||||
|
||||
/// Meta-learning parameter optimizer.
|
||||
pub struct MetaAdapter {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Tunable parameters.
|
||||
params: [TunableParam; NUM_PARAMS],
|
||||
|
||||
@@ -140,6 +142,7 @@ impl MetaAdapter {
|
||||
/// 7: intrusion_sensitivity (0.30, range 0.05-0.9)
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
params: [
|
||||
TunableParam::new(0.05, 0.01, 0.50, 0.01),
|
||||
TunableParam::new(0.10, 0.02, 1.00, 0.02),
|
||||
@@ -198,7 +201,6 @@ impl MetaAdapter {
|
||||
///
|
||||
/// Returns events as `(event_id, value)` pairs.
|
||||
pub fn on_timer(&mut self) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
self.eval_ticks += 1;
|
||||
@@ -228,16 +230,14 @@ impl MetaAdapter {
|
||||
self.consecutive_failures = 0;
|
||||
self.success_count += 1;
|
||||
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (
|
||||
EVENT_PARAM_ADJUSTED,
|
||||
self.current_param as f32
|
||||
+ self.params[self.current_param].value / 1000.0,
|
||||
);
|
||||
n_ev += 1;
|
||||
EVENTS[n_ev] = (EVENT_ADAPTATION_SCORE, score);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (
|
||||
EVENT_PARAM_ADJUSTED,
|
||||
self.current_param as f32
|
||||
+ self.params[self.current_param].value / 1000.0,
|
||||
);
|
||||
n_ev += 1;
|
||||
self.events[n_ev] = (EVENT_ADAPTATION_SCORE, score);
|
||||
n_ev += 1;
|
||||
} else {
|
||||
// Revert the perturbation.
|
||||
self.params[self.current_param].value =
|
||||
@@ -248,10 +248,8 @@ impl MetaAdapter {
|
||||
// ── Safety rollback ──────────────────────────────────
|
||||
if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
|
||||
self.safety_rollback();
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_ROLLBACK_TRIGGERED, self.meta_level as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_ROLLBACK_TRIGGERED, self.meta_level as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
|
||||
// ── Advance to next parameter ────────────────────────
|
||||
@@ -261,16 +259,14 @@ impl MetaAdapter {
|
||||
|
||||
// ── Emit meta level periodically ─────────────────────
|
||||
if self.sweep_idx == 0 && n_ev < 4 {
|
||||
unsafe {
|
||||
EVENTS[n_ev] = (EVENT_META_LEVEL, self.meta_level as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
self.events[n_ev] = (EVENT_META_LEVEL, self.meta_level as f32);
|
||||
n_ev += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
/// Compute the performance score from accumulated feedback.
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
//! Cardiac arrhythmia detection — ADR-041 Category 1 Medical module.
|
||||
//! Cardiac-rhythm anomaly flagging — ADR-041 Category 1 Medical module.
|
||||
//!
|
||||
//! Monitors heart rate from host CSI pipeline and detects:
|
||||
//! - Tachycardia: sustained HR > 100 BPM
|
||||
//! - Bradycardia: sustained HR < 50 BPM
|
||||
//! - Missed beats: sudden HR dips > 30% below running average
|
||||
//! - HRV anomaly: RMSSD outside normal range over 30-second window
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA.
|
||||
//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis or patient monitoring.
|
||||
//! ⚠️ This module flags *candidate* arrhythmia-like heart-rate signatures only
|
||||
//! ⚠️ (sustained high/low rate estimates, abrupt drops, variability proxies);
|
||||
//! ⚠️ it has never been compared against ECG or any reference standard, and its
|
||||
//! ⚠️ accuracy is unproven (see ADR-160 §A1). Gated behind the non-default
|
||||
//! ⚠️ `medical-experimental` cargo feature.
|
||||
//!
|
||||
//! Monitors a heart-rate estimate from the host CSI pipeline and flags:
|
||||
//! - Tachycardia-like: sustained rate estimate > 100 BPM
|
||||
//! - Bradycardia-like: sustained rate estimate < 50 BPM
|
||||
//! - Missed-beat-like: sudden rate dips > 30% below running average
|
||||
//! - HRV-like anomaly: RMSSD proxy outside a coarse band over 30 seconds
|
||||
//!
|
||||
//! These are experimental signal proxies, NOT clinical measurements.
|
||||
//!
|
||||
//! Events:
|
||||
//! TACHYCARDIA (110) — sustained high heart rate
|
||||
@@ -87,6 +97,8 @@ pub struct CardiacArrhythmiaDetector {
|
||||
cd_hrv: u16,
|
||||
/// Frame counter.
|
||||
frame_count: u32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl CardiacArrhythmiaDetector {
|
||||
@@ -106,6 +118,7 @@ impl CardiacArrhythmiaDetector {
|
||||
cd_missed: 0,
|
||||
cd_hrv: 0,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,14 +135,13 @@ impl CardiacArrhythmiaDetector {
|
||||
self.cd_missed = self.cd_missed.saturating_sub(1);
|
||||
self.cd_hrv = self.cd_hrv.saturating_sub(1);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
|
||||
// Ignore invalid / zero / NaN readings.
|
||||
// NaN comparisons return false, so we must check explicitly to prevent
|
||||
// NaN from contaminating the EMA and RMSSD calculations.
|
||||
if !(hr_bpm >= 1.0) {
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
|
||||
// ── EMA update ──────────────────────────────────────────────────
|
||||
@@ -156,7 +168,7 @@ impl CardiacArrhythmiaDetector {
|
||||
if hr_bpm > TACHY_THRESH {
|
||||
self.tachy_count = self.tachy_count.saturating_add(1);
|
||||
if self.tachy_count >= SUSTAINED_SECS && self.cd_tachy == 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_TACHYCARDIA, hr_bpm); }
|
||||
self.events[n] = (EVENT_TACHYCARDIA, hr_bpm);
|
||||
n += 1;
|
||||
self.cd_tachy = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -168,7 +180,7 @@ impl CardiacArrhythmiaDetector {
|
||||
if hr_bpm < BRADY_THRESH {
|
||||
self.brady_count = self.brady_count.saturating_add(1);
|
||||
if self.brady_count >= SUSTAINED_SECS && self.cd_brady == 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_BRADYCARDIA, hr_bpm); }
|
||||
self.events[n] = (EVENT_BRADYCARDIA, hr_bpm);
|
||||
n += 1;
|
||||
self.cd_brady = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -180,7 +192,7 @@ impl CardiacArrhythmiaDetector {
|
||||
if self.ema_init && self.hr_ema > 1.0 {
|
||||
let drop_frac = (self.hr_ema - hr_bpm) / self.hr_ema;
|
||||
if drop_frac > MISSED_BEAT_DROP && self.cd_missed == 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_MISSED_BEAT, hr_bpm); }
|
||||
self.events[n] = (EVENT_MISSED_BEAT, hr_bpm);
|
||||
n += 1;
|
||||
self.cd_missed = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -190,13 +202,13 @@ impl CardiacArrhythmiaDetector {
|
||||
if self.rr_len >= HRV_WINDOW && n < 4 {
|
||||
let rmssd = self.compute_rmssd();
|
||||
if (rmssd < RMSSD_LOW || rmssd > RMSSD_HIGH) && self.cd_hrv == 0 {
|
||||
unsafe { EVENTS[n] = (EVENT_HRV_ANOMALY, rmssd); }
|
||||
self.events[n] = (EVENT_HRV_ANOMALY, rmssd);
|
||||
n += 1;
|
||||
self.cd_hrv = COOLDOWN_SECS;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Compute RMSSD from the RR-diff ring buffer.
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
//! Gait analysis — ADR-041 Category 1 Medical module.
|
||||
//! Gait-parameter proxies & fall-risk-like scoring — ADR-041 Category 1 Medical module.
|
||||
//!
|
||||
//! Extracts gait parameters from CSI phase variance periodicity to assess
|
||||
//! mobility and fall risk:
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA.
|
||||
//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, fall-risk assessment, or
|
||||
//! ⚠️ any clinical decision. This module computes *candidate* gait-parameter
|
||||
//! ⚠️ proxies and a fall-risk-like score only; it has never been compared
|
||||
//! ⚠️ against gait labs, clinical fall-risk instruments, or any reference
|
||||
//! ⚠️ standard, and its accuracy is unproven (see ADR-160 §A1). Gated behind
|
||||
//! ⚠️ the non-default `medical-experimental` cargo feature.
|
||||
//!
|
||||
//! Extracts candidate gait-parameter proxies from CSI phase-variance
|
||||
//! periodicity (experimental, NOT clinical measurements):
|
||||
//! - Step cadence (steps/min) from dominant phase variance frequency
|
||||
//! - Gait asymmetry from left/right step interval ratio
|
||||
//! - Stride variability (coefficient of variation)
|
||||
@@ -109,6 +117,9 @@ pub struct GaitAnalyzer {
|
||||
|
||||
/// Frame counter.
|
||||
frame_count: u32,
|
||||
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 5],
|
||||
}
|
||||
|
||||
impl GaitAnalyzer {
|
||||
@@ -132,6 +143,7 @@ impl GaitAnalyzer {
|
||||
last_asymmetry: 0.0,
|
||||
last_fall_risk: 0.0,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 5],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +174,6 @@ impl GaitAnalyzer {
|
||||
self.var_idx = (self.var_idx + 1) % GAIT_WINDOW;
|
||||
if self.var_len < GAIT_WINDOW { self.var_len += 1; }
|
||||
|
||||
static mut EVENTS: [(i32, f32); 5] = [(0, 0.0); 5];
|
||||
let mut n = 0usize;
|
||||
|
||||
// ── Step detection (peak in variance) ───────────────────────────
|
||||
@@ -201,13 +212,13 @@ impl GaitAnalyzer {
|
||||
|
||||
// Emit cadence.
|
||||
if n < 5 {
|
||||
unsafe { EVENTS[n] = (EVENT_STEP_CADENCE, cadence); }
|
||||
self.events[n] = (EVENT_STEP_CADENCE, cadence);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
// Emit asymmetry if above threshold.
|
||||
if fabsf(asymmetry - 1.0) > ASYMMETRY_THRESH && n < 5 {
|
||||
unsafe { EVENTS[n] = (EVENT_GAIT_ASYMMETRY, asymmetry); }
|
||||
self.events[n] = (EVENT_GAIT_ASYMMETRY, asymmetry);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -215,7 +226,7 @@ impl GaitAnalyzer {
|
||||
if cadence > SHUFFLE_CADENCE_HIGH && avg_energy < SHUFFLE_ENERGY_LOW
|
||||
&& self.cd_shuffle == 0 && n < 5
|
||||
{
|
||||
unsafe { EVENTS[n] = (EVENT_SHUFFLING_DETECTED, cadence); }
|
||||
self.events[n] = (EVENT_SHUFFLING_DETECTED, cadence);
|
||||
n += 1;
|
||||
self.cd_shuffle = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -223,7 +234,7 @@ impl GaitAnalyzer {
|
||||
// Festination: accelerating cadence.
|
||||
if self.cadence_len >= 3 && self.cd_festination == 0 && n < 5 {
|
||||
if self.detect_festination() {
|
||||
unsafe { EVENTS[n] = (EVENT_FESTINATION, cadence); }
|
||||
self.events[n] = (EVENT_FESTINATION, cadence);
|
||||
n += 1;
|
||||
self.cd_festination = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -233,7 +244,7 @@ impl GaitAnalyzer {
|
||||
let risk = self.compute_fall_risk(cadence, asymmetry, variability, avg_energy);
|
||||
self.last_fall_risk = risk;
|
||||
if n < 5 {
|
||||
unsafe { EVENTS[n] = (EVENT_FALL_RISK_SCORE, risk); }
|
||||
self.events[n] = (EVENT_FALL_RISK_SCORE, risk);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -241,7 +252,7 @@ impl GaitAnalyzer {
|
||||
self.step_count = 0;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Compute cadence in steps/min from step intervals.
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
//! Respiratory distress detection — ADR-041 Category 1 Medical module.
|
||||
//! Respiratory-distress-like pattern flagging — ADR-041 Category 1 Medical module.
|
||||
//!
|
||||
//! Detects pathological breathing patterns from host CSI pipeline:
|
||||
//! - Tachypnea: sustained breathing rate > 25 BPM
|
||||
//! - Labored breathing: high amplitude variance relative to baseline
|
||||
//! - Cheyne-Stokes respiration: crescendo-decrescendo periodicity (30-90 s)
|
||||
//! detected via autocorrelation of the breathing amplitude envelope
|
||||
//! - Overall respiratory distress level: composite severity score 0-100
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA.
|
||||
//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis or patient monitoring.
|
||||
//! ⚠️ This module flags *candidate* respiratory-distress-like breathing
|
||||
//! ⚠️ signatures only; it has never been compared against capnography,
|
||||
//! ⚠️ spirometry, or any reference standard, and its accuracy is unproven
|
||||
//! ⚠️ (see ADR-160 §A1). Gated behind the non-default `medical-experimental`
|
||||
//! ⚠️ cargo feature.
|
||||
//!
|
||||
//! Flags candidate pathological-breathing-like patterns from the host CSI
|
||||
//! pipeline (experimental proxies, NOT clinical measurements):
|
||||
//! - Tachypnea-like: sustained breathing-rate estimate > 25 BPM
|
||||
//! - Labored-breathing-like: high amplitude variance relative to baseline
|
||||
//! - Cheyne-Stokes-like: crescendo-decrescendo periodicity (30-90 s)
|
||||
//! flagged via autocorrelation of the breathing-rate envelope
|
||||
//! - Composite distress-level proxy: severity score 0-100
|
||||
//!
|
||||
//! Events:
|
||||
//! TACHYPNEA (120) — sustained high respiratory rate
|
||||
@@ -97,6 +106,9 @@ pub struct RespiratoryDistressDetector {
|
||||
|
||||
/// Frame counter.
|
||||
frame_count: u32,
|
||||
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl RespiratoryDistressDetector {
|
||||
@@ -116,6 +128,7 @@ impl RespiratoryDistressDetector {
|
||||
cd_cs: 0,
|
||||
last_distress: 0.0,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,14 +176,13 @@ impl RespiratoryDistressDetector {
|
||||
self.var_mean += d / self.var_count as f32;
|
||||
}
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
|
||||
// ── Tachypnea ───────────────────────────────────────────────────
|
||||
if breathing_bpm > TACHYPNEA_THRESH {
|
||||
self.tachy_count = self.tachy_count.saturating_add(1);
|
||||
if self.tachy_count >= SUSTAINED_SECS && self.cd_tachy == 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_TACHYPNEA, breathing_bpm); }
|
||||
self.events[n] = (EVENT_TACHYPNEA, breathing_bpm);
|
||||
n += 1;
|
||||
self.cd_tachy = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -183,7 +195,7 @@ impl RespiratoryDistressDetector {
|
||||
let current_var = self.recent_var_mean();
|
||||
let ratio = current_var / self.var_mean;
|
||||
if ratio > LABORED_VAR_RATIO && self.cd_labored == 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_LABORED_BREATHING, ratio); }
|
||||
self.events[n] = (EVENT_LABORED_BREATHING, ratio);
|
||||
n += 1;
|
||||
self.cd_labored = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -192,7 +204,7 @@ impl RespiratoryDistressDetector {
|
||||
// ── Cheyne-Stokes (autocorrelation) ─────────────────────────────
|
||||
if self.bpm_len >= AC_WINDOW && self.cd_cs == 0 && n < 4 {
|
||||
if let Some(period) = self.detect_cheyne_stokes() {
|
||||
unsafe { EVENTS[n] = (EVENT_CHEYNE_STOKES, period as f32); }
|
||||
self.events[n] = (EVENT_CHEYNE_STOKES, period as f32);
|
||||
n += 1;
|
||||
self.cd_cs = COOLDOWN_SECS;
|
||||
}
|
||||
@@ -202,11 +214,11 @@ impl RespiratoryDistressDetector {
|
||||
if self.frame_count % DISTRESS_REPORT_INTERVAL == 0 && n < 4 {
|
||||
let score = self.compute_distress_score(breathing_bpm, variance);
|
||||
self.last_distress = score;
|
||||
unsafe { EVENTS[n] = (EVENT_RESP_DISTRESS_LEVEL, score); }
|
||||
self.events[n] = (EVENT_RESP_DISTRESS_LEVEL, score);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Mean of recent variance samples.
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
//! Seizure detection — ADR-041 Category 1 Medical module.
|
||||
//! Seizure-like motion-signature flagging — ADR-041 Category 1 Medical module.
|
||||
//!
|
||||
//! Detects tonic-clonic seizures via high-energy rhythmic motion in the
|
||||
//! 3-8 Hz band, discriminating from:
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA.
|
||||
//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, seizure monitoring, or any
|
||||
//! ⚠️ clinical decision. This module flags *candidate* seizure-like motion
|
||||
//! ⚠️ signatures (high-energy rhythmic 3-8 Hz motion) only; it has never been
|
||||
//! ⚠️ validated against EEG/video-EEG or any reference standard, and its
|
||||
//! ⚠️ accuracy is unproven (see ADR-160 §A1). Seizure detection cannot be
|
||||
//! ⚠️ validated without clinical data — this module does not claim to do so.
|
||||
//! ⚠️ Gated behind the non-default `medical-experimental` cargo feature.
|
||||
//!
|
||||
//! Flags candidate tonic-clonic-seizure-like motion signatures (experimental)
|
||||
//! via high-energy rhythmic motion in the 3-8 Hz band, attempting to
|
||||
//! discriminate from:
|
||||
//! - Falls: single impulse followed by stillness
|
||||
//! - Tremor: lower amplitude, higher regularity
|
||||
//!
|
||||
@@ -125,6 +135,9 @@ pub struct SeizureDetector {
|
||||
|
||||
/// Frame counter.
|
||||
frame_count: u32,
|
||||
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl SeizureDetector {
|
||||
@@ -143,6 +156,7 @@ impl SeizureDetector {
|
||||
cooldown: 0,
|
||||
seizure_count: 0,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +186,6 @@ impl SeizureDetector {
|
||||
self.amp_idx = (self.amp_idx + 1) % PHASE_WINDOW;
|
||||
if self.amp_len < PHASE_WINDOW { self.amp_len += 1; }
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
|
||||
// No detection without presence.
|
||||
@@ -182,7 +195,7 @@ impl SeizureDetector {
|
||||
self.state_frames = 0;
|
||||
self.high_energy_frames = 0;
|
||||
}
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
|
||||
// Tick cooldown.
|
||||
@@ -192,7 +205,7 @@ impl SeizureDetector {
|
||||
self.phase = SeizurePhase::Monitoring;
|
||||
self.state_frames = 0;
|
||||
}
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
|
||||
// ── State machine ───────────────────────────────────────────────
|
||||
@@ -222,7 +235,7 @@ impl SeizureDetector {
|
||||
self.phase = SeizurePhase::Monitoring;
|
||||
self.state_frames = 0;
|
||||
self.high_energy_frames = 0;
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +245,7 @@ impl SeizureDetector {
|
||||
self.phase = SeizurePhase::Tonic;
|
||||
self.state_frames = 0;
|
||||
self.seizure_count += 1;
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_ONSET, motion_energy); }
|
||||
self.events[n] = (EVENT_SEIZURE_ONSET, motion_energy);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -244,10 +257,10 @@ impl SeizureDetector {
|
||||
self.phase = SeizurePhase::Clonic;
|
||||
self.state_frames = 0;
|
||||
self.seizure_count += 1;
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_ONSET, motion_energy); }
|
||||
self.events[n] = (EVENT_SEIZURE_ONSET, motion_energy);
|
||||
n += 1;
|
||||
if n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_CLONIC, period as f32); }
|
||||
self.events[n] = (EVENT_SEIZURE_CLONIC, period as f32);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -271,13 +284,13 @@ impl SeizureDetector {
|
||||
if energy_var > TONIC_VAR_CEIL {
|
||||
if let Some(period) = self.detect_rhythm() {
|
||||
if self.state_frames >= TONIC_MIN_FRAMES && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); }
|
||||
self.events[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32);
|
||||
n += 1;
|
||||
}
|
||||
self.phase = SeizurePhase::Clonic;
|
||||
self.state_frames = 0;
|
||||
if n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_CLONIC, period as f32); }
|
||||
self.events[n] = (EVENT_SEIZURE_CLONIC, period as f32);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -289,7 +302,7 @@ impl SeizureDetector {
|
||||
self.low_energy_frames += 1;
|
||||
if self.low_energy_frames >= POST_ICTAL_MIN_FRAMES {
|
||||
if self.state_frames >= TONIC_MIN_FRAMES && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32); }
|
||||
self.events[n] = (EVENT_SEIZURE_TONIC, self.state_frames as f32);
|
||||
n += 1;
|
||||
}
|
||||
self.phase = SeizurePhase::PostIctal;
|
||||
@@ -318,7 +331,7 @@ impl SeizureDetector {
|
||||
SeizurePhase::PostIctal => {
|
||||
self.state_frames += 1;
|
||||
if self.state_frames == 1 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_POST_ICTAL, 1.0); }
|
||||
self.events[n] = (EVENT_POST_ICTAL, 1.0);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
@@ -337,7 +350,7 @@ impl SeizureDetector {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Compute variance of recent motion energy.
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
//! Sleep apnea detection — ADR-041 Category 1 Medical module.
|
||||
//! Apnea-like breathing-pause flagging — ADR-041 Category 1 Medical module.
|
||||
//!
|
||||
//! Detects obstructive and central sleep apnea by monitoring breathing BPM
|
||||
//! from the host CSI pipeline. When breathing drops below 4 BPM for more
|
||||
//! than 10 seconds the detector flags an apnea event. It also tracks the
|
||||
//! Apnea-Hypopnea Index (AHI) — the number of apnea events per hour of
|
||||
//! monitored sleep time.
|
||||
//! ⚠️ EXPERIMENTAL RESEARCH MODULE — NOT VALIDATED AGAINST CLINICAL DATA.
|
||||
//! ⚠️ NOT A MEDICAL DEVICE. Do NOT use for diagnosis, monitoring of patients,
|
||||
//! ⚠️ or any clinical decision. This module flags *candidate* apnea-like
|
||||
//! ⚠️ breathing-pause signatures (sustained low breathing-rate estimates)
|
||||
//! ⚠️ only; it has never been compared against polysomnography or any
|
||||
//! ⚠️ reference standard, and its accuracy is unproven (see ADR-160 §A1).
|
||||
//! ⚠️ Gated behind the non-default `medical-experimental` cargo feature so it
|
||||
//! ⚠️ cannot be silently built into a shipping artifact.
|
||||
//!
|
||||
//! Monitors breathing-rate estimates from the host CSI pipeline. When the
|
||||
//! estimate drops below 4 BPM for more than 10 seconds the detector flags a
|
||||
//! candidate apnea-like event. It also tracks a candidate Apnea-Hypopnea
|
||||
//! Index (AHI) proxy — the number of flagged events per hour of monitored
|
||||
//! time. These are experimental proxies, NOT clinical measurements.
|
||||
//!
|
||||
//! Events:
|
||||
//! APNEA_START (100) — breathing ceased or fell below threshold
|
||||
@@ -77,6 +86,8 @@ pub struct SleepApneaDetector {
|
||||
timer_count: u32,
|
||||
/// Most recently computed AHI.
|
||||
last_ahi: f32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl SleepApneaDetector {
|
||||
@@ -90,6 +101,7 @@ impl SleepApneaDetector {
|
||||
monitoring_secs: 0,
|
||||
timer_count: 0,
|
||||
last_ahi: 0.0,
|
||||
events: [(0, 0.0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +116,6 @@ impl SleepApneaDetector {
|
||||
) -> &[(i32, f32)] {
|
||||
self.timer_count += 1;
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
|
||||
// Only monitor when subject is present.
|
||||
@@ -115,11 +126,11 @@ impl SleepApneaDetector {
|
||||
self.record_episode(self.current_start, dur);
|
||||
self.in_apnea = false;
|
||||
self.low_breath_secs = 0;
|
||||
unsafe { EVENTS[n] = (EVENT_APNEA_END, dur as f32); }
|
||||
self.events[n] = (EVENT_APNEA_END, dur as f32);
|
||||
n += 1;
|
||||
}
|
||||
self.low_breath_secs = 0;
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
|
||||
self.monitoring_secs += 1;
|
||||
@@ -129,7 +140,7 @@ impl SleepApneaDetector {
|
||||
// Treat NaN as invalid — skip detection for this frame.
|
||||
if breathing_bpm != breathing_bpm {
|
||||
// NaN: f32::NAN != f32::NAN is true.
|
||||
return unsafe { &EVENTS[..n] };
|
||||
return &self.events[..n];
|
||||
}
|
||||
|
||||
// ── Apnea detection ─────────────────────────────────────────────
|
||||
@@ -140,7 +151,7 @@ impl SleepApneaDetector {
|
||||
// Apnea onset — backdate start to when breathing first dropped.
|
||||
self.in_apnea = true;
|
||||
self.current_start = self.timer_count.saturating_sub(self.low_breath_secs);
|
||||
unsafe { EVENTS[n] = (EVENT_APNEA_START, breathing_bpm); }
|
||||
self.events[n] = (EVENT_APNEA_START, breathing_bpm);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -149,7 +160,7 @@ impl SleepApneaDetector {
|
||||
let dur = self.timer_count.saturating_sub(self.current_start);
|
||||
self.record_episode(self.current_start, dur);
|
||||
self.in_apnea = false;
|
||||
unsafe { EVENTS[n] = (EVENT_APNEA_END, dur as f32); }
|
||||
self.events[n] = (EVENT_APNEA_END, dur as f32);
|
||||
n += 1;
|
||||
}
|
||||
self.low_breath_secs = 0;
|
||||
@@ -163,11 +174,11 @@ impl SleepApneaDetector {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
unsafe { EVENTS[n] = (EVENT_AHI_UPDATE, self.last_ahi); }
|
||||
self.events[n] = (EVENT_AHI_UPDATE, self.last_ahi);
|
||||
n += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
fn record_episode(&mut self, start: u32, duration: u32) {
|
||||
|
||||
@@ -42,6 +42,8 @@ struct ZoneState {
|
||||
|
||||
/// Occupancy zone detector.
|
||||
pub struct OccupancyDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 12],
|
||||
zones: [ZoneState; MAX_ZONES],
|
||||
n_zones: usize,
|
||||
/// Calibration accumulators.
|
||||
@@ -61,6 +63,7 @@ impl OccupancyDetector {
|
||||
prev_occupied: false,
|
||||
};
|
||||
Self {
|
||||
events: [(0, 0.0); 12],
|
||||
zones: [ZONE_INIT; MAX_ZONES],
|
||||
n_zones: 0,
|
||||
calib_sum: [0.0; MAX_ZONES],
|
||||
@@ -163,7 +166,6 @@ impl OccupancyDetector {
|
||||
|
||||
// Build output events in a static buffer.
|
||||
// We re-use a static to avoid allocation in no_std.
|
||||
static mut EVENTS: [(i32, f32); 12] = [(0, 0.0); 12];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Emit per-zone occupancy (every 10 frames to limit bandwidth).
|
||||
@@ -172,18 +174,14 @@ impl OccupancyDetector {
|
||||
if self.zones[z].occupied && n_events < 10 {
|
||||
// Encode zone_id in integer part, confidence in fractional.
|
||||
let val = z as f32 + self.zones[z].score.min(0.99);
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ZONE_OCCUPIED, val);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ZONE_OCCUPIED, val);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit total occupied zone count.
|
||||
if n_events < 11 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ZONE_COUNT, total_occupied as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ZONE_COUNT, total_occupied as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -192,14 +190,12 @@ impl OccupancyDetector {
|
||||
for z in 0..zone_count {
|
||||
if self.zones[z].occupied != self.zones[z].prev_occupied && n_events < 12 {
|
||||
let val = z as f32 + if self.zones[z].occupied { 0.5 } else { 0.0 };
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ZONE_TRANSITION, val);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ZONE_TRANSITION, val);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Get the number of currently occupied zones.
|
||||
|
||||
@@ -112,6 +112,8 @@ impl Hypothesis {
|
||||
|
||||
/// Grover-inspired room state search engine.
|
||||
pub struct InterferenceSearch {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Amplitude for each of the 16 hypotheses.
|
||||
amplitudes: [f32; N_HYPO],
|
||||
/// Total Grover iterations applied.
|
||||
@@ -130,6 +132,7 @@ impl InterferenceSearch {
|
||||
pub const fn new() -> Self {
|
||||
// 1/sqrt(16) = 0.25
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
amplitudes: [0.25; N_HYPO],
|
||||
iteration_count: 0,
|
||||
converged: false,
|
||||
@@ -178,37 +181,30 @@ impl InterferenceSearch {
|
||||
self.converged = winner_prob > CONVERGENCE_PROB;
|
||||
|
||||
// ── Build output events ──
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Emit winner periodically or on change.
|
||||
let winner_changed = winner_idx as u8 != self.prev_winner;
|
||||
if winner_changed || self.frame_count % WINNER_EMIT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_HYPOTHESIS_WINNER, winner_idx as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_HYPOTHESIS_WINNER, winner_idx as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Emit amplitude periodically.
|
||||
if self.frame_count % AMPLITUDE_EMIT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_HYPOTHESIS_AMPLITUDE, winner_prob);
|
||||
}
|
||||
self.events[n_events] = (EVENT_HYPOTHESIS_AMPLITUDE, winner_prob);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Emit iteration count periodically.
|
||||
if self.frame_count % ITERATION_EMIT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_SEARCH_ITERATIONS, self.iteration_count as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_SEARCH_ITERATIONS, self.iteration_count as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
self.prev_winner = winner_idx as u8;
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Apply the oracle: set boost/dampen factors based on CSI evidence.
|
||||
|
||||
@@ -58,6 +58,8 @@ pub const EVENT_BLOCH_DRIFT: i32 = 852;
|
||||
|
||||
/// Quantum-inspired coherence monitor using Bloch sphere representation.
|
||||
pub struct QuantumCoherenceMonitor {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Previous aggregate Bloch vector [x, y, z].
|
||||
prev_bloch: [f32; 3],
|
||||
/// EMA-smoothed Von Neumann entropy.
|
||||
@@ -74,6 +76,7 @@ impl QuantumCoherenceMonitor {
|
||||
/// Create a new monitor. Const-evaluable for static initialization.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
prev_bloch: [0.0, 0.0, 1.0],
|
||||
smoothed_entropy: 0.0,
|
||||
prev_entropy: 0.0,
|
||||
@@ -129,34 +132,27 @@ impl QuantumCoherenceMonitor {
|
||||
self.prev_bloch = bloch;
|
||||
|
||||
// ── Build output events ──
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Entropy (periodic).
|
||||
if self.frame_count % ENTROPY_EMIT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_ENTANGLEMENT_ENTROPY, self.smoothed_entropy);
|
||||
}
|
||||
self.events[n_events] = (EVENT_ENTANGLEMENT_ENTROPY, self.smoothed_entropy);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Decoherence event (immediate).
|
||||
if entropy_jump > DECOHERENCE_THRESHOLD {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_DECOHERENCE_EVENT, entropy_jump);
|
||||
}
|
||||
self.events[n_events] = (EVENT_DECOHERENCE_EVENT, entropy_jump);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// Bloch drift (periodic).
|
||||
if self.frame_count % DRIFT_EMIT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_BLOCH_DRIFT, drift);
|
||||
}
|
||||
self.events[n_events] = (EVENT_BLOCH_DRIFT, drift);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Compute the mean Bloch vector from subcarrier phases.
|
||||
|
||||
@@ -72,6 +72,8 @@ const MAX_EVENTS: usize = 4;
|
||||
|
||||
/// Tracks directional foot traffic using phase gradient analysis.
|
||||
pub struct CustomerFlowTracker {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
/// Previous phase values per subcarrier.
|
||||
prev_phases: [f32; MAX_SC],
|
||||
/// Previous amplitude values per subcarrier.
|
||||
@@ -101,6 +103,7 @@ pub struct CustomerFlowTracker {
|
||||
impl CustomerFlowTracker {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
prev_phases: [0.0; MAX_SC],
|
||||
prev_amplitudes: [0.0; MAX_SC],
|
||||
gradient_ema: Ema::new(GRADIENT_EMA_ALPHA),
|
||||
@@ -200,7 +203,6 @@ impl CustomerFlowTracker {
|
||||
}
|
||||
|
||||
// Build events.
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Crossing detection: look for gradient peak + motion + amplitude spike.
|
||||
@@ -218,9 +220,7 @@ impl CustomerFlowTracker {
|
||||
self.ingress_count += 1;
|
||||
self.hourly_ingress += 1;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_INGRESS, self.ingress_count as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_INGRESS, self.ingress_count as f32);
|
||||
ne += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -228,9 +228,7 @@ impl CustomerFlowTracker {
|
||||
self.egress_count += 1;
|
||||
self.hourly_egress += 1;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_EGRESS, self.egress_count as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_EGRESS, self.egress_count as f32);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -238,9 +236,7 @@ impl CustomerFlowTracker {
|
||||
// Emit net occupancy on each crossing.
|
||||
let net = self.net_occupancy();
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_NET_OCCUPANCY, net as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_NET_OCCUPANCY, net as f32);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -248,9 +244,7 @@ impl CustomerFlowTracker {
|
||||
// Periodic net occupancy report.
|
||||
if self.frame_count % OCCUPANCY_REPORT_INTERVAL == 0 && ne < MAX_EVENTS {
|
||||
let net = self.net_occupancy();
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_NET_OCCUPANCY, net as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_NET_OCCUPANCY, net as f32);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -259,16 +253,14 @@ impl CustomerFlowTracker {
|
||||
// Encode: ingress * 1000 + egress.
|
||||
let summary = self.hourly_ingress as f32 * 1000.0 + self.hourly_egress as f32;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_HOURLY_TRAFFIC, summary);
|
||||
}
|
||||
self.events[ne] = (EVENT_HOURLY_TRAFFIC, summary);
|
||||
ne += 1;
|
||||
}
|
||||
self.hourly_ingress = 0;
|
||||
self.hourly_egress = 0;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Get net occupancy (ingress - egress), clamped to 0.
|
||||
|
||||
@@ -80,6 +80,8 @@ const ZONE_INIT: ZoneState = ZoneState {
|
||||
|
||||
/// Tracks dwell time across a 3x3 spatial zone grid.
|
||||
pub struct DwellHeatmapTracker {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
zones: [ZoneState; NUM_ZONES],
|
||||
/// Frame counter.
|
||||
frame_count: u32,
|
||||
@@ -96,6 +98,7 @@ pub struct DwellHeatmapTracker {
|
||||
impl DwellHeatmapTracker {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
zones: [ZONE_INIT; NUM_ZONES],
|
||||
frame_count: 0,
|
||||
any_present: false,
|
||||
@@ -176,7 +179,6 @@ impl DwellHeatmapTracker {
|
||||
self.any_present = is_present || any_zone_occupied;
|
||||
|
||||
// Build events.
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Periodic zone updates.
|
||||
@@ -186,9 +188,7 @@ impl DwellHeatmapTracker {
|
||||
if self.zones[z].dwell_seconds > 0.0 && ne < MAX_EVENTS - 3 {
|
||||
// Encode zone_id in integer part, dwell seconds in value.
|
||||
let val = z as f32 * 1000.0 + self.zones[z].dwell_seconds;
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_DWELL_ZONE_UPDATE, val);
|
||||
}
|
||||
self.events[ne] = (EVENT_DWELL_ZONE_UPDATE, val);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -211,16 +211,12 @@ impl DwellHeatmapTracker {
|
||||
}
|
||||
|
||||
if hot_dwell > 0.0 && ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_HOT_ZONE, hot_zone as f32 + hot_dwell / 1000.0);
|
||||
}
|
||||
self.events[ne] = (EVENT_HOT_ZONE, hot_zone as f32 + hot_dwell / 1000.0);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
if cold_dwell < f32::MAX && ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_COLD_ZONE, cold_zone as f32 + cold_dwell / 1000.0);
|
||||
}
|
||||
self.events[ne] = (EVENT_COLD_ZONE, cold_zone as f32 + cold_dwell / 1000.0);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -230,14 +226,12 @@ impl DwellHeatmapTracker {
|
||||
self.session_active = false;
|
||||
let session_duration = (self.frame_count - self.session_start_frame) as f32 / FRAME_RATE;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_SESSION_SUMMARY, session_duration);
|
||||
}
|
||||
self.events[ne] = (EVENT_SESSION_SUMMARY, session_duration);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Get dwell time (seconds) for a specific zone in the current session.
|
||||
|
||||
@@ -62,6 +62,8 @@ const RATE_HISTORY: usize = 1200;
|
||||
|
||||
/// Estimates queue length from CSI presence and person-count data.
|
||||
pub struct QueueLengthEstimator {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Smoothed queue length estimate.
|
||||
queue_ema: Ema,
|
||||
/// Smoothed arrival rate (persons/minute).
|
||||
@@ -91,6 +93,7 @@ pub struct QueueLengthEstimator {
|
||||
impl QueueLengthEstimator {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
queue_ema: Ema::new(QUEUE_EMA_ALPHA),
|
||||
arrival_rate_ema: Ema::new(RATE_EMA_ALPHA),
|
||||
service_rate_ema: Ema::new(RATE_EMA_ALPHA),
|
||||
@@ -161,14 +164,11 @@ impl QueueLengthEstimator {
|
||||
}
|
||||
|
||||
// Build events.
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Periodic queue length report.
|
||||
if self.frame_count % REPORT_INTERVAL == 0 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_QUEUE_LENGTH, self.current_queue as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_QUEUE_LENGTH, self.current_queue as f32);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -184,9 +184,7 @@ impl QueueLengthEstimator {
|
||||
|
||||
// Service rate event.
|
||||
if ne < 4 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_SERVICE_RATE, self.service_rate_ema.value);
|
||||
}
|
||||
self.events[ne] = (EVENT_SERVICE_RATE, self.service_rate_ema.value);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -199,9 +197,7 @@ impl QueueLengthEstimator {
|
||||
};
|
||||
|
||||
if ne < 4 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_WAIT_TIME_ESTIMATE, wait_time);
|
||||
}
|
||||
self.events[ne] = (EVENT_WAIT_TIME_ESTIMATE, wait_time);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -216,16 +212,14 @@ impl QueueLengthEstimator {
|
||||
if self.current_queue as f32 >= QUEUE_ALERT_THRESH && !self.alert_active {
|
||||
self.alert_active = true;
|
||||
if ne < 4 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_QUEUE_ALERT, self.current_queue as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_QUEUE_ALERT, self.current_queue as f32);
|
||||
ne += 1;
|
||||
}
|
||||
} else if (self.current_queue as f32) < QUEUE_ALERT_THRESH - 1.0 {
|
||||
self.alert_active = false;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Get the current smoothed queue length.
|
||||
|
||||
@@ -96,6 +96,8 @@ pub enum EngagementLevel {
|
||||
|
||||
/// Detects and classifies customer shelf engagement from CSI data.
|
||||
pub struct ShelfEngagementDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
/// Previous phase values for perturbation calculation.
|
||||
prev_phases: [f32; MAX_SC],
|
||||
/// Phase perturbation EMA (high-frequency component).
|
||||
@@ -133,6 +135,7 @@ pub struct ShelfEngagementDetector {
|
||||
impl ShelfEngagementDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
prev_phases: [0.0; MAX_SC],
|
||||
perturbation_ema: Ema::new(PERTURBATION_EMA_ALPHA),
|
||||
motion_ema: Ema::new(MOTION_EMA_ALPHA),
|
||||
@@ -221,7 +224,6 @@ impl ShelfEngagementDetector {
|
||||
self.phase_diff_history.push(perturbation);
|
||||
|
||||
// Build events.
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut ne = 0usize;
|
||||
|
||||
if !is_present {
|
||||
@@ -234,7 +236,7 @@ impl ShelfEngagementDetector {
|
||||
self.still_frames = 0;
|
||||
self.level = EngagementLevel::None;
|
||||
self.prev_emitted_level = EngagementLevel::None;
|
||||
unsafe { return &EVENTS[..ne]; }
|
||||
return &self.events[..ne];
|
||||
}
|
||||
|
||||
// Detect stillness (low translational motion).
|
||||
@@ -249,7 +251,7 @@ impl ShelfEngagementDetector {
|
||||
self.engagement_frames = 0;
|
||||
self.level = EngagementLevel::None;
|
||||
self.prev_emitted_level = EngagementLevel::None;
|
||||
unsafe { return &EVENTS[..ne]; }
|
||||
return &self.events[..ne];
|
||||
}
|
||||
|
||||
// Only start engagement counting after debounce.
|
||||
@@ -284,9 +286,7 @@ impl ShelfEngagementDetector {
|
||||
};
|
||||
|
||||
if event_id != 0 && ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (event_id, duration);
|
||||
}
|
||||
self.events[ne] = (event_id, duration);
|
||||
ne += 1;
|
||||
self.prev_emitted_level = self.level;
|
||||
self.cooldown = ENGAGEMENT_COOLDOWN;
|
||||
@@ -297,13 +297,11 @@ impl ShelfEngagementDetector {
|
||||
// Reach detection: sudden high-frequency phase burst while still.
|
||||
if self.still_frames > STILL_DEBOUNCE && perturbation > REACH_BURST_THRESH && ne < MAX_EVENTS {
|
||||
self.total_reaches += 1;
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_REACH_DETECTED, perturbation);
|
||||
}
|
||||
self.events[ne] = (EVENT_REACH_DETECTED, perturbation);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Emit engagement end event based on current level.
|
||||
|
||||
@@ -80,6 +80,8 @@ pub enum TableState {
|
||||
|
||||
/// Tracks table occupancy state transitions and turnover metrics.
|
||||
pub struct TableTurnoverTracker {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); MAX_EVENTS],
|
||||
/// Current table state.
|
||||
state: TableState,
|
||||
/// Smoothed motion energy.
|
||||
@@ -109,6 +111,7 @@ pub struct TableTurnoverTracker {
|
||||
impl TableTurnoverTracker {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); MAX_EVENTS],
|
||||
state: TableState::Empty,
|
||||
motion_ema: Ema::new(MOTION_EMA_ALPHA),
|
||||
presence_frames: 0,
|
||||
@@ -143,7 +146,6 @@ impl TableTurnoverTracker {
|
||||
let smoothed_motion = self.motion_ema.update(motion_energy);
|
||||
let n = if n_persons < 0 { 0 } else { n_persons };
|
||||
|
||||
static mut EVENTS: [(i32, f32); MAX_EVENTS] = [(0, 0.0); MAX_EVENTS];
|
||||
let mut ne = 0usize;
|
||||
|
||||
match self.state {
|
||||
@@ -158,9 +160,7 @@ impl TableTurnoverTracker {
|
||||
self.absence_frames = 0;
|
||||
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TABLE_SEATED, n as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_TABLE_SEATED, n as f32);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -202,9 +202,7 @@ impl TableTurnoverTracker {
|
||||
let duration_s = self.session_frames as f32 / FRAME_RATE;
|
||||
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TABLE_VACATED, duration_s);
|
||||
}
|
||||
self.events[ne] = (EVENT_TABLE_VACATED, duration_s);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -241,9 +239,7 @@ impl TableTurnoverTracker {
|
||||
|
||||
let duration_s = self.session_frames as f32 / FRAME_RATE;
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TABLE_VACATED, duration_s);
|
||||
}
|
||||
self.events[ne] = (EVENT_TABLE_VACATED, duration_s);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -270,9 +266,7 @@ impl TableTurnoverTracker {
|
||||
self.peak_persons = 0;
|
||||
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TABLE_AVAILABLE, 1.0);
|
||||
}
|
||||
self.events[ne] = (EVENT_TABLE_AVAILABLE, 1.0);
|
||||
ne += 1;
|
||||
}
|
||||
} else if is_present {
|
||||
@@ -285,9 +279,7 @@ impl TableTurnoverTracker {
|
||||
self.presence_frames = 0;
|
||||
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TABLE_SEATED, n as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_TABLE_SEATED, n as f32);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -301,14 +293,12 @@ impl TableTurnoverTracker {
|
||||
if self.frame_count % TURNOVER_REPORT_INTERVAL == 0 && self.frame_count > 0 {
|
||||
let rate = self.turnover_rate();
|
||||
if ne < MAX_EVENTS {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TURNOVER_RATE, rate);
|
||||
}
|
||||
self.events[ne] = (EVENT_TURNOVER_RATE, rate);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Compute turnovers per hour (rolling window).
|
||||
|
||||
@@ -46,6 +46,8 @@ pub enum LoiterState {
|
||||
|
||||
/// Loitering detector.
|
||||
pub struct LoiteringDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 2],
|
||||
state: LoiterState,
|
||||
/// Consecutive frames with presence detected.
|
||||
presence_frames: u32,
|
||||
@@ -65,6 +67,7 @@ pub struct LoiteringDetector {
|
||||
impl LoiteringDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 2],
|
||||
state: LoiterState::Absent,
|
||||
presence_frames: 0,
|
||||
dwell_frames: 0,
|
||||
@@ -88,7 +91,6 @@ impl LoiteringDetector {
|
||||
self.frame_count += 1;
|
||||
self.post_end_cd = self.post_end_cd.saturating_sub(1);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 2] = [(0, 0.0); 2];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Determine if someone is present and roughly stationary.
|
||||
@@ -133,9 +135,7 @@ impl LoiteringDetector {
|
||||
|
||||
if ne < 2 {
|
||||
let dwell_seconds = self.dwell_frames as f32 / 20.0;
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_LOITERING_START, dwell_seconds);
|
||||
}
|
||||
self.events[ne] = (EVENT_LOITERING_START, dwell_seconds);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -161,9 +161,7 @@ impl LoiteringDetector {
|
||||
self.ongoing_timer = 0;
|
||||
if ne < 2 {
|
||||
let total_seconds = self.dwell_frames as f32 / 20.0;
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_LOITERING_ONGOING, total_seconds);
|
||||
}
|
||||
self.events[ne] = (EVENT_LOITERING_ONGOING, total_seconds);
|
||||
ne += 1;
|
||||
}
|
||||
}
|
||||
@@ -177,9 +175,7 @@ impl LoiteringDetector {
|
||||
|
||||
if ne < 2 {
|
||||
let total_seconds = self.dwell_frames as f32 / 20.0;
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_LOITERING_END, total_seconds);
|
||||
}
|
||||
self.events[ne] = (EVENT_LOITERING_END, total_seconds);
|
||||
ne += 1;
|
||||
}
|
||||
|
||||
@@ -191,7 +187,7 @@ impl LoiteringDetector {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn state(&self) -> LoiterState { self.state }
|
||||
|
||||
@@ -54,6 +54,8 @@ pub const EVENT_FLEEING_DETECTED: i32 = 252;
|
||||
|
||||
/// Panic/erratic motion detector.
|
||||
pub struct PanicMotionDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Circular buffer of motion energy values.
|
||||
energy_buf: [f32; WINDOW],
|
||||
/// Circular buffer of phase variance values (for direction estimation).
|
||||
@@ -75,6 +77,7 @@ pub struct PanicMotionDetector {
|
||||
impl PanicMotionDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
energy_buf: [0.0; WINDOW],
|
||||
variance_buf: [0.0; WINDOW],
|
||||
buf_idx: 0,
|
||||
@@ -102,7 +105,6 @@ impl PanicMotionDetector {
|
||||
self.cd_struggle = self.cd_struggle.saturating_sub(1);
|
||||
self.cd_fleeing = self.cd_fleeing.saturating_sub(1);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Store in circular buffer.
|
||||
@@ -117,13 +119,13 @@ impl PanicMotionDetector {
|
||||
if !self.buf_filled {
|
||||
self.prev_energy = motion_energy;
|
||||
self.prev_energy_init = true;
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Require presence.
|
||||
if presence < MIN_PRESENCE {
|
||||
self.prev_energy = motion_energy;
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Compute jerk (absolute rate of change of motion energy).
|
||||
@@ -142,7 +144,7 @@ impl PanicMotionDetector {
|
||||
|
||||
// Skip if not enough motion.
|
||||
if mean_energy < MIN_MOTION {
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Panic detection: high jerk AND high entropy over threshold fraction of window.
|
||||
@@ -152,7 +154,7 @@ impl PanicMotionDetector {
|
||||
|
||||
if is_panic && self.cd_panic == 0 && ne < 3 {
|
||||
let severity = (mean_jerk / JERK_THRESH) * (entropy / ENTROPY_THRESH);
|
||||
unsafe { EVENTS[ne] = (EVENT_PANIC_DETECTED, severity.min(10.0)); }
|
||||
self.events[ne] = (EVENT_PANIC_DETECTED, severity.min(10.0));
|
||||
ne += 1;
|
||||
self.cd_panic = COOLDOWN;
|
||||
self.panic_count += 1;
|
||||
@@ -167,7 +169,7 @@ impl PanicMotionDetector {
|
||||
&& entropy > ENTROPY_THRESH * 0.5;
|
||||
|
||||
if is_struggle && !is_panic && self.cd_struggle == 0 && ne < 3 {
|
||||
unsafe { EVENTS[ne] = (EVENT_STRUGGLE_PATTERN, mean_jerk); }
|
||||
self.events[ne] = (EVENT_STRUGGLE_PATTERN, mean_jerk);
|
||||
ne += 1;
|
||||
self.cd_struggle = COOLDOWN;
|
||||
}
|
||||
@@ -179,12 +181,12 @@ impl PanicMotionDetector {
|
||||
&& entropy < FLEE_MAX_ENTROPY;
|
||||
|
||||
if is_fleeing && !is_panic && self.cd_fleeing == 0 && ne < 3 {
|
||||
unsafe { EVENTS[ne] = (EVENT_FLEEING_DETECTED, mean_energy); }
|
||||
self.events[ne] = (EVENT_FLEEING_DETECTED, mean_energy);
|
||||
ne += 1;
|
||||
self.cd_fleeing = COOLDOWN;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Compute window-level statistics.
|
||||
|
||||
@@ -92,6 +92,8 @@ impl ZoneState {
|
||||
|
||||
/// Multi-zone perimeter breach detector.
|
||||
pub struct PerimeterBreachDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
zones: [ZoneState; MAX_ZONES],
|
||||
/// Calibration accumulators per zone: sum of gradient magnitudes.
|
||||
cal_grad_sum: [f32; MAX_ZONES],
|
||||
@@ -118,6 +120,7 @@ pub struct PerimeterBreachDetector {
|
||||
impl PerimeterBreachDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
zones: [ZoneState::new(); MAX_ZONES],
|
||||
cal_grad_sum: [0.0; MAX_ZONES],
|
||||
cal_var_sum: [0.0; MAX_ZONES],
|
||||
@@ -155,7 +158,6 @@ impl PerimeterBreachDetector {
|
||||
self.cd_departure = self.cd_departure.saturating_sub(1);
|
||||
self.cd_transition = self.cd_transition.saturating_sub(1);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
|
||||
let subs_per_zone = n_sc / MAX_ZONES;
|
||||
@@ -196,7 +198,7 @@ impl PerimeterBreachDetector {
|
||||
}
|
||||
if !self.phase_init {
|
||||
self.phase_init = true;
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Calibration phase.
|
||||
@@ -214,7 +216,7 @@ impl PerimeterBreachDetector {
|
||||
}
|
||||
self.calibrated = true;
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Detect breaches and direction per zone.
|
||||
@@ -262,7 +264,7 @@ impl PerimeterBreachDetector {
|
||||
if self.approach_run[z] >= DIRECTION_DEBOUNCE && is_breach
|
||||
&& self.cd_approach == 0 && ne < 4
|
||||
{
|
||||
unsafe { EVENTS[ne] = (EVENT_APPROACH_DETECTED, z as f32); }
|
||||
self.events[ne] = (EVENT_APPROACH_DETECTED, z as f32);
|
||||
ne += 1;
|
||||
self.cd_approach = COOLDOWN;
|
||||
self.approach_run[z] = 0;
|
||||
@@ -272,7 +274,7 @@ impl PerimeterBreachDetector {
|
||||
if self.departure_run[z] >= DIRECTION_DEBOUNCE
|
||||
&& self.cd_departure == 0 && ne < 4
|
||||
{
|
||||
unsafe { EVENTS[ne] = (EVENT_DEPARTURE_DETECTED, z as f32); }
|
||||
self.events[ne] = (EVENT_DEPARTURE_DETECTED, z as f32);
|
||||
ne += 1;
|
||||
self.cd_departure = COOLDOWN;
|
||||
self.departure_run[z] = 0;
|
||||
@@ -281,7 +283,7 @@ impl PerimeterBreachDetector {
|
||||
|
||||
// Perimeter breach event.
|
||||
if most_disturbed_zone >= 0 && self.cd_breach == 0 && ne < 4 {
|
||||
unsafe { EVENTS[ne] = (EVENT_PERIMETER_BREACH, max_energy); }
|
||||
self.events[ne] = (EVENT_PERIMETER_BREACH, max_energy);
|
||||
ne += 1;
|
||||
self.cd_breach = COOLDOWN;
|
||||
}
|
||||
@@ -296,7 +298,7 @@ impl PerimeterBreachDetector {
|
||||
// Encode as from*10 + to.
|
||||
let transition_code = self.last_active_zone as f32 * 10.0
|
||||
+ most_disturbed_zone as f32;
|
||||
unsafe { EVENTS[ne] = (EVENT_ZONE_TRANSITION, transition_code); }
|
||||
self.events[ne] = (EVENT_ZONE_TRANSITION, transition_code);
|
||||
ne += 1;
|
||||
self.cd_transition = COOLDOWN;
|
||||
}
|
||||
@@ -305,7 +307,7 @@ impl PerimeterBreachDetector {
|
||||
self.last_active_zone = most_disturbed_zone;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn is_calibrated(&self) -> bool { self.calibrated }
|
||||
|
||||
@@ -50,6 +50,8 @@ enum PeakState {
|
||||
|
||||
/// Tailgating detector.
|
||||
pub struct TailgateDetector {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
state: PeakState,
|
||||
/// Current peak's maximum energy.
|
||||
peak_max: f32,
|
||||
@@ -80,6 +82,7 @@ pub struct TailgateDetector {
|
||||
impl TailgateDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
state: PeakState::Idle,
|
||||
peak_max: 0.0,
|
||||
peak_frames: 0,
|
||||
@@ -110,7 +113,6 @@ impl TailgateDetector {
|
||||
self.cd_tailgate = self.cd_tailgate.saturating_sub(1);
|
||||
self.cd_passage = self.cd_passage.saturating_sub(1);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut ne = 0usize;
|
||||
|
||||
// Update noise floor estimate (exponential moving average of variance).
|
||||
@@ -168,7 +170,7 @@ impl TailgateDetector {
|
||||
self.state = PeakState::InPeak;
|
||||
self.peak_max = motion_energy;
|
||||
self.peak_frames = 1;
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Window expired — evaluate passage.
|
||||
@@ -176,9 +178,7 @@ impl TailgateDetector {
|
||||
if self.peaks_in_window >= 2 {
|
||||
// Multiple peaks detected = tailgating.
|
||||
if self.cd_tailgate == 0 && ne < 3 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_TAILGATE_DETECTED, self.peaks_in_window as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_TAILGATE_DETECTED, self.peaks_in_window as f32);
|
||||
ne += 1;
|
||||
self.cd_tailgate = COOLDOWN;
|
||||
self.tailgate_count += 1;
|
||||
@@ -186,18 +186,14 @@ impl TailgateDetector {
|
||||
|
||||
// Also emit multi-passage.
|
||||
if self.cd_passage == 0 && ne < 3 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_MULTI_PASSAGE, self.peaks_in_window as f32);
|
||||
}
|
||||
self.events[ne] = (EVENT_MULTI_PASSAGE, self.peaks_in_window as f32);
|
||||
ne += 1;
|
||||
self.cd_passage = COOLDOWN;
|
||||
}
|
||||
} else if self.peaks_in_window == 1 {
|
||||
// Single passage.
|
||||
if self.cd_passage == 0 && ne < 3 {
|
||||
unsafe {
|
||||
EVENTS[ne] = (EVENT_SINGLE_PASSAGE, self.peak_energies[0]);
|
||||
}
|
||||
self.events[ne] = (EVENT_SINGLE_PASSAGE, self.peak_energies[0]);
|
||||
ne += 1;
|
||||
self.cd_passage = COOLDOWN;
|
||||
self.single_passages += 1;
|
||||
@@ -212,7 +208,7 @@ impl TailgateDetector {
|
||||
}
|
||||
|
||||
self.prev_energy = motion_energy;
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn frame_count(&self) -> u32 { self.frame_count }
|
||||
|
||||
@@ -11,7 +11,14 @@
|
||||
//! 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).
|
||||
//! ⚠️ HONEST-NAMING NOTE (ADR-160 §A3): this module measures RF **reflectivity**
|
||||
//! ⚠️ (an amplitude-variance / phase-variance ratio), not weapons. A variance
|
||||
//! ⚠️ ratio cannot discriminate a weapon from any other highly-reflective metal
|
||||
//! ⚠️ object (keys, laptop, belt buckle). The high-ratio event is therefore named
|
||||
//! ⚠️ `HIGH_METAL_REFLECTIVITY`, NOT a weapon alert — the physical quantity the
|
||||
//! ⚠️ code can actually back.
|
||||
//!
|
||||
//! Events: METAL_ANOMALY(220), HIGH_METAL_REFLECTIVITY(221), CALIBRATION_NEEDED(222).
|
||||
//! Budget: S (<5 ms).
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
@@ -26,16 +33,17 @@ const MAX_SC: usize = 32;
|
||||
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;
|
||||
/// Elevated reflectivity-ratio threshold (very high RF reflectivity).
|
||||
/// NOTE (ADR-160 §A3): a variance ratio measures reflectivity, not weapons.
|
||||
const HIGH_REFLECTIVITY_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;
|
||||
/// Consecutive frames for high-reflectivity debounce.
|
||||
const HIGH_REFLECTIVITY_DEBOUNCE: u8 = 6;
|
||||
/// Cooldown frames after event emission.
|
||||
const COOLDOWN: u16 = 60;
|
||||
/// Re-calibration trigger: if baseline drift exceeds this ratio.
|
||||
@@ -44,7 +52,9 @@ const RECALIB_DRIFT_THRESH: f32 = 3.0;
|
||||
const VAR_WINDOW: usize = 16;
|
||||
|
||||
pub const EVENT_METAL_ANOMALY: i32 = 220;
|
||||
pub const EVENT_WEAPON_ALERT: i32 = 221;
|
||||
/// High RF reflectivity (formerly mislabelled `EVENT_WEAPON_ALERT`, ADR-160 §A3).
|
||||
/// A variance ratio measures reflectivity, not weapon-grade discrimination.
|
||||
pub const EVENT_HIGH_METAL_REFLECTIVITY: i32 = 221;
|
||||
pub const EVENT_CALIBRATION_NEEDED: i32 = 222;
|
||||
|
||||
/// Concealed metallic object detector.
|
||||
@@ -74,12 +84,14 @@ pub struct WeaponDetector {
|
||||
run_count: u32,
|
||||
/// Debounce counters.
|
||||
metal_run: u8,
|
||||
weapon_run: u8,
|
||||
high_refl_run: u8,
|
||||
/// Cooldowns.
|
||||
cd_metal: u16,
|
||||
cd_weapon: u16,
|
||||
cd_high_refl: u16,
|
||||
cd_recalib: u16,
|
||||
frame_count: u32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
}
|
||||
|
||||
impl WeaponDetector {
|
||||
@@ -101,11 +113,12 @@ impl WeaponDetector {
|
||||
run_phase_m2: [0.0; MAX_SC],
|
||||
run_count: 0,
|
||||
metal_run: 0,
|
||||
weapon_run: 0,
|
||||
high_refl_run: 0,
|
||||
cd_metal: 0,
|
||||
cd_weapon: 0,
|
||||
cd_high_refl: 0,
|
||||
cd_recalib: 0,
|
||||
frame_count: 0,
|
||||
events: [(0, 0.0); 3],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,10 +138,9 @@ impl WeaponDetector {
|
||||
|
||||
self.frame_count += 1;
|
||||
self.cd_metal = self.cd_metal.saturating_sub(1);
|
||||
self.cd_weapon = self.cd_weapon.saturating_sub(1);
|
||||
self.cd_high_refl = self.cd_high_refl.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.
|
||||
@@ -153,7 +165,7 @@ impl WeaponDetector {
|
||||
}
|
||||
self.calibrated = true;
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Update running Welford statistics.
|
||||
@@ -176,7 +188,7 @@ impl WeaponDetector {
|
||||
// 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;
|
||||
self.high_refl_run = 0;
|
||||
// Reset running stats periodically when no one is present.
|
||||
if self.run_count > 200 {
|
||||
self.run_count = 0;
|
||||
@@ -187,12 +199,12 @@ impl WeaponDetector {
|
||||
self.run_phase_m2[i] = 0.0;
|
||||
}
|
||||
}
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
// Compute current amplitude variance / phase variance ratio.
|
||||
if self.run_count < 4 {
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.events[..0];
|
||||
}
|
||||
|
||||
let mut ratio_sum = 0.0f32;
|
||||
@@ -221,14 +233,14 @@ impl WeaponDetector {
|
||||
}
|
||||
|
||||
if valid_sc < 2 {
|
||||
return unsafe { &EVENTS[..0] };
|
||||
return &self.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); }
|
||||
self.events[ne] = (EVENT_CALIBRATION_NEEDED, max_drift);
|
||||
ne += 1;
|
||||
self.cd_recalib = COOLDOWN * 5; // Less frequent recalibration alerts.
|
||||
}
|
||||
@@ -240,28 +252,28 @@ impl WeaponDetector {
|
||||
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);
|
||||
// High-reflectivity detection (higher threshold). NOT weapon discrimination.
|
||||
if mean_ratio > HIGH_REFLECTIVITY_THRESH {
|
||||
self.high_refl_run = self.high_refl_run.saturating_add(1);
|
||||
} else {
|
||||
self.weapon_run = self.weapon_run.saturating_sub(1);
|
||||
self.high_refl_run = self.high_refl_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); }
|
||||
self.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); }
|
||||
// Emit high-reflectivity event (supersedes metal anomaly in severity).
|
||||
if self.high_refl_run >= HIGH_REFLECTIVITY_DEBOUNCE && self.cd_high_refl == 0 && ne < 3 {
|
||||
self.events[ne] = (EVENT_HIGH_METAL_REFLECTIVITY, mean_ratio);
|
||||
ne += 1;
|
||||
self.cd_weapon = COOLDOWN;
|
||||
self.cd_high_refl = COOLDOWN;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn is_calibrated(&self) -> bool { self.calibrated }
|
||||
@@ -311,7 +323,7 @@ mod tests {
|
||||
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);
|
||||
assert_ne!(et, EVENT_HIGH_METAL_REFLECTIVITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,7 +381,7 @@ mod tests {
|
||||
}
|
||||
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");
|
||||
assert_ne!(et, EVENT_HIGH_METAL_REFLECTIVITY, "normal person should not trigger weapon alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ impl WelfordStats {
|
||||
|
||||
/// Coherence-gated frame filter.
|
||||
pub struct CoherenceGate {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
prev_phases: [f32; MAX_SC],
|
||||
stats: WelfordStats,
|
||||
initial_variance: f32,
|
||||
@@ -89,6 +91,7 @@ pub struct CoherenceGate {
|
||||
impl CoherenceGate {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
prev_phases: [0.0; MAX_SC],
|
||||
stats: WelfordStats::new(),
|
||||
initial_variance: 0.0,
|
||||
@@ -105,7 +108,6 @@ impl CoherenceGate {
|
||||
let n_sc = if phases.len() > MAX_SC { MAX_SC } else { phases.len() };
|
||||
if n_sc < 2 { return &[]; }
|
||||
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_ev = 0usize;
|
||||
|
||||
if !self.initialized {
|
||||
@@ -146,7 +148,7 @@ impl CoherenceGate {
|
||||
self.gate = GateDecision::Recalibrate;
|
||||
self.low_count = 0;
|
||||
self.high_count = 0;
|
||||
unsafe { EVENTS[n_ev] = (EVENT_RECALIBRATE_NEEDED, variance); }
|
||||
self.events[n_ev] = (EVENT_RECALIBRATE_NEEDED, variance);
|
||||
n_ev += 1;
|
||||
} else {
|
||||
let below = coherence < LOW_THRESHOLD;
|
||||
@@ -178,11 +180,11 @@ impl CoherenceGate {
|
||||
};
|
||||
}
|
||||
|
||||
unsafe { EVENTS[n_ev] = (EVENT_GATE_DECISION, self.gate.as_f32()); }
|
||||
self.events[n_ev] = (EVENT_GATE_DECISION, self.gate.as_f32());
|
||||
n_ev += 1;
|
||||
unsafe { EVENTS[n_ev] = (EVENT_COHERENCE_SCORE, coherence); }
|
||||
self.events[n_ev] = (EVENT_COHERENCE_SCORE, coherence);
|
||||
n_ev += 1;
|
||||
unsafe { &EVENTS[..n_ev] }
|
||||
&self.events[..n_ev]
|
||||
}
|
||||
|
||||
pub fn gate(&self) -> GateDecision { self.gate }
|
||||
|
||||
@@ -25,6 +25,8 @@ pub const EVENT_SPATIAL_FOCUS_ZONE: i32 = 702;
|
||||
|
||||
/// Flash Attention spatial focus estimator.
|
||||
pub struct FlashAttention {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
prev_group_phases: [f32; N_GROUPS],
|
||||
attention_weights: [f32; N_GROUPS],
|
||||
smoothed_entropy: f32,
|
||||
@@ -37,6 +39,7 @@ pub struct FlashAttention {
|
||||
impl FlashAttention {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
prev_group_phases: [0.0; N_GROUPS],
|
||||
attention_weights: [0.0; N_GROUPS],
|
||||
smoothed_entropy: MAX_ENTROPY,
|
||||
@@ -50,7 +53,6 @@ impl FlashAttention {
|
||||
let n_sc = phases.len().min(amplitudes.len()).min(MAX_SC);
|
||||
if n_sc < N_GROUPS { return &[]; }
|
||||
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
|
||||
// Per-group means for Q and V.
|
||||
let subs_per = n_sc / N_GROUPS;
|
||||
@@ -117,12 +119,10 @@ impl FlashAttention {
|
||||
for g in 0..N_GROUPS { self.prev_group_phases[g] = q[g]; }
|
||||
|
||||
// Emit events.
|
||||
unsafe {
|
||||
EVENTS[0] = (EVENT_ATTENTION_PEAK_SC, peak_idx as f32);
|
||||
EVENTS[1] = (EVENT_ATTENTION_SPREAD, self.smoothed_entropy);
|
||||
EVENTS[2] = (EVENT_SPATIAL_FOCUS_ZONE, centroid);
|
||||
&EVENTS[..3]
|
||||
}
|
||||
self.events[0] = (EVENT_ATTENTION_PEAK_SC, peak_idx as f32);
|
||||
self.events[1] = (EVENT_ATTENTION_SPREAD, self.smoothed_entropy);
|
||||
self.events[2] = (EVENT_SPATIAL_FOCUS_ZONE, centroid);
|
||||
&self.events[..3]
|
||||
}
|
||||
|
||||
pub fn weights(&self) -> &[f32; N_GROUPS] { &self.attention_weights }
|
||||
|
||||
@@ -59,6 +59,8 @@ impl PersonSlot {
|
||||
|
||||
/// Min-cut person identity matcher.
|
||||
pub struct PersonMatcher {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 8],
|
||||
slots: [PersonSlot; MAX_PERSONS],
|
||||
active_count: u8,
|
||||
prev_assignment: [u8; MAX_PERSONS],
|
||||
@@ -69,6 +71,7 @@ pub struct PersonMatcher {
|
||||
impl PersonMatcher {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 8],
|
||||
slots: [
|
||||
PersonSlot::new(0),
|
||||
PersonSlot::new(1),
|
||||
@@ -98,7 +101,6 @@ impl PersonMatcher {
|
||||
self.frame_count += 1;
|
||||
let n_det = n_persons.min(MAX_PERSONS);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Extract per-person feature vectors (spatial region -> top-8 variances).
|
||||
@@ -134,9 +136,7 @@ impl PersonMatcher {
|
||||
self.swap_count += 1;
|
||||
if n_events < 7 {
|
||||
let swap_val = (prev as f32) * 16.0 + (curr as f32);
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_PERSON_ID_SWAP, swap_val);
|
||||
}
|
||||
self.events[n_events] = (EVENT_PERSON_ID_SWAP, swap_val);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -177,9 +177,7 @@ impl PersonMatcher {
|
||||
0.0
|
||||
};
|
||||
let val = slot.person_id as f32 + confidence.min(0.99) * 0.01;
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_PERSON_ID_ASSIGNED, val);
|
||||
}
|
||||
self.events[n_events] = (EVENT_PERSON_ID_ASSIGNED, val);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -213,9 +211,7 @@ impl PersonMatcher {
|
||||
avg_conf /= n_det as f32;
|
||||
|
||||
if n_events < 8 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_MATCH_CONFIDENCE, avg_conf);
|
||||
}
|
||||
self.events[n_events] = (EVENT_MATCH_CONFIDENCE, avg_conf);
|
||||
n_events += 1;
|
||||
}
|
||||
}
|
||||
@@ -223,7 +219,7 @@ impl PersonMatcher {
|
||||
// Save current assignment for next-frame swap detection.
|
||||
self.prev_assignment = assignment;
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Extract top-FEAT_DIM variance values (descending) from a subcarrier range.
|
||||
|
||||
@@ -84,12 +84,15 @@ pub struct OptimalTransportDetector {
|
||||
frame_count: u32,
|
||||
shift_streak: u8,
|
||||
subtle_streak: u8,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
}
|
||||
|
||||
impl OptimalTransportDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self { prev_amps: [0.0; MAX_SC], smoothed_dist: 0.0, smoothed_var: 0.0, prev_var: 0.0,
|
||||
initialized: false, frame_count: 0, shift_streak: 0, subtle_streak: 0 }
|
||||
initialized: false, frame_count: 0, shift_streak: 0, subtle_streak: 0,
|
||||
events: [(0, 0.0); 4] }
|
||||
}
|
||||
|
||||
fn w1_sorted(a: &[f32], b: &[f32], n: usize) -> f32 {
|
||||
@@ -150,16 +153,15 @@ impl OptimalTransportDetector {
|
||||
|
||||
i = 0; while i < n { self.prev_amps[i] = cur[i]; i += 1; }
|
||||
|
||||
static mut EV: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
|
||||
if self.frame_count % 5 == 0 && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_WASSERSTEIN_DISTANCE, self.smoothed_dist); } ne += 1;
|
||||
self.events[ne] = (EVENT_WASSERSTEIN_DISTANCE, self.smoothed_dist); ne += 1;
|
||||
}
|
||||
if self.smoothed_dist > WASS_SHIFT {
|
||||
self.shift_streak = self.shift_streak.saturating_add(1);
|
||||
if self.shift_streak >= SHIFT_DEB && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_DISTRIBUTION_SHIFT, self.smoothed_dist); } ne += 1;
|
||||
self.events[ne] = (EVENT_DISTRIBUTION_SHIFT, self.smoothed_dist); ne += 1;
|
||||
self.shift_streak = 0;
|
||||
}
|
||||
} else { self.shift_streak = 0; }
|
||||
@@ -167,12 +169,12 @@ impl OptimalTransportDetector {
|
||||
if self.smoothed_dist > WASS_SUBTLE && vc < VAR_STABLE {
|
||||
self.subtle_streak = self.subtle_streak.saturating_add(1);
|
||||
if self.subtle_streak >= SUBTLE_DEB && ne < 4 {
|
||||
unsafe { EV[ne] = (EVENT_SUBTLE_MOTION, self.smoothed_dist); } ne += 1;
|
||||
self.events[ne] = (EVENT_SUBTLE_MOTION, self.smoothed_dist); ne += 1;
|
||||
self.subtle_streak = 0;
|
||||
}
|
||||
} else { self.subtle_streak = 0; }
|
||||
|
||||
unsafe { &EV[..ne] }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
pub fn distance(&self) -> f32 { self.smoothed_dist }
|
||||
|
||||
@@ -64,6 +64,8 @@ fn soft_threshold(x: f32, t: f32) -> f32 {
|
||||
|
||||
/// Sparse subcarrier recovery engine.
|
||||
pub struct SparseRecovery {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 3],
|
||||
/// Compact correlation estimate: [MAX_SC][NEIGHBORS].
|
||||
/// For subcarrier i: [corr(i,i-1), corr(i,i), corr(i,i+1)].
|
||||
/// Edge entries (i=0 left neighbor, i=31 right neighbor) are zero.
|
||||
@@ -87,6 +89,7 @@ pub struct SparseRecovery {
|
||||
impl SparseRecovery {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 3],
|
||||
correlation: [[0.0; NEIGHBORS]; MAX_SC],
|
||||
recent_valid: [0.0; MAX_SC],
|
||||
initialized: false,
|
||||
@@ -135,20 +138,17 @@ impl SparseRecovery {
|
||||
}
|
||||
|
||||
// -- Build event output -----------------------------------------------
|
||||
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
|
||||
let mut n_events = 0usize;
|
||||
|
||||
// Always emit dropout rate periodically (every 20 frames).
|
||||
if self.frame_count % 20 == 0 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_DROPOUT_RATE, dropout_rate);
|
||||
}
|
||||
self.events[n_events] = (EVENT_DROPOUT_RATE, dropout_rate);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
// -- Skip recovery if dropout too low or model not ready ---------------
|
||||
if dropout_rate < MIN_DROPOUT_RATE || !self.initialized {
|
||||
unsafe { return &EVENTS[..n_events]; }
|
||||
return &self.events[..n_events];
|
||||
}
|
||||
|
||||
// -- ISTA recovery ----------------------------------------------------
|
||||
@@ -158,19 +158,15 @@ impl SparseRecovery {
|
||||
|
||||
// Emit recovery results.
|
||||
if n_events < 3 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_RECOVERY_COMPLETE, recovered as f32);
|
||||
}
|
||||
self.events[n_events] = (EVENT_RECOVERY_COMPLETE, recovered as f32);
|
||||
n_events += 1;
|
||||
}
|
||||
if n_events < 3 {
|
||||
unsafe {
|
||||
EVENTS[n_events] = (EVENT_RECOVERY_ERROR, residual);
|
||||
}
|
||||
self.events[n_events] = (EVENT_RECOVERY_ERROR, residual);
|
||||
n_events += 1;
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n_events] }
|
||||
&self.events[..n_events]
|
||||
}
|
||||
|
||||
/// Update the compact correlation model from a fully valid frame.
|
||||
|
||||
@@ -54,12 +54,16 @@ pub struct TemporalCompressor {
|
||||
prev_ts: u32,
|
||||
has_ts: bool,
|
||||
ratio: f32,
|
||||
/// Per-call event scratch buffers (owned; replace former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
timer_events: [(i32, f32); 2],
|
||||
}
|
||||
|
||||
impl TemporalCompressor {
|
||||
pub const fn new() -> Self {
|
||||
const E: Snap = Snap::empty();
|
||||
Self { buf: [E; CAP], w_idx: 0, total: 0, frame_rate: 20.0, prev_ts: 0, has_ts: false, ratio: 1.0 }
|
||||
Self { buf: [E; CAP], w_idx: 0, total: 0, frame_rate: 20.0, prev_ts: 0, has_ts: false, ratio: 1.0,
|
||||
events: [(0, 0.0); 4], timer_events: [(0, 0.0); 2] }
|
||||
}
|
||||
|
||||
fn occ(&self) -> usize { if (self.total as usize) < CAP { self.total as usize } else { CAP } }
|
||||
@@ -97,7 +101,6 @@ impl TemporalCompressor {
|
||||
}
|
||||
self.prev_ts = ts_ms; self.has_ts = true;
|
||||
|
||||
static mut EV: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut ne = 0usize;
|
||||
let occ = self.occ();
|
||||
|
||||
@@ -113,23 +116,22 @@ impl TemporalCompressor {
|
||||
let mut j = 0;
|
||||
while j < VALS { let d = dequantize(self.buf[slot].data[j], s, old_l); self.buf[slot].data[j] = quantize(d, s, new_l); j += 1; }
|
||||
self.buf[slot].tier = new_t;
|
||||
if ne < 4 { unsafe { EV[ne] = (EVENT_TIER_TRANSITION, new_t as i32 as f32); } ne += 1; }
|
||||
if ne < 4 { self.events[ne] = (EVENT_TIER_TRANSITION, new_t as i32 as f32); ne += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ratio = self.calc_ratio(occ);
|
||||
if self.total % 64 == 0 && ne < 4 { unsafe { EV[ne] = (EVENT_COMPRESSION_RATIO, self.ratio); } ne += 1; }
|
||||
unsafe { &EV[..ne] }
|
||||
if self.total % 64 == 0 && ne < 4 { self.events[ne] = (EVENT_COMPRESSION_RATIO, self.ratio); ne += 1; }
|
||||
&self.events[..ne]
|
||||
}
|
||||
|
||||
/// Periodic timer events.
|
||||
pub fn on_timer(&self) -> &[(i32, f32)] {
|
||||
static mut TE: [(i32, f32); 2] = [(0, 0.0); 2];
|
||||
pub fn on_timer(&mut self) -> &[(i32, f32)] {
|
||||
let mut n = 0;
|
||||
let h = self.history_hours();
|
||||
if h > 0.0 { unsafe { TE[n] = (EVENT_HISTORY_DEPTH_HOURS, h); } n += 1; }
|
||||
unsafe { TE[n] = (EVENT_COMPRESSION_RATIO, self.ratio); } n += 1;
|
||||
unsafe { &TE[..n] }
|
||||
if h > 0.0 { self.timer_events[n] = (EVENT_HISTORY_DEPTH_HOURS, h); n += 1; }
|
||||
self.timer_events[n] = (EVENT_COMPRESSION_RATIO, self.ratio); n += 1;
|
||||
&self.timer_events[..n]
|
||||
}
|
||||
|
||||
fn calc_ratio(&self, occ: usize) -> f32 {
|
||||
|
||||
@@ -56,6 +56,8 @@ fn l2_query(stored: &[f32; DIM], query: &[f32]) -> f32 {
|
||||
|
||||
/// Micro-HNSW on-device vector index.
|
||||
pub struct MicroHnsw {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
nodes: [HnswNode; MAX_VECTORS],
|
||||
n_vectors: usize,
|
||||
entry_point: usize,
|
||||
@@ -68,6 +70,7 @@ impl MicroHnsw {
|
||||
pub const fn new() -> Self {
|
||||
const EMPTY: HnswNode = HnswNode::empty();
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
nodes: [EMPTY; MAX_VECTORS], n_vectors: 0, entry_point: usize::MAX,
|
||||
frame_count: 0, last_nearest: 0, last_distance: f32::MAX,
|
||||
}
|
||||
@@ -194,9 +197,8 @@ impl MicroHnsw {
|
||||
pub fn process_frame(&mut self, features: &[f32]) -> &[(i32, f32)] {
|
||||
self.frame_count += 1;
|
||||
if self.n_vectors == 0 {
|
||||
static mut EMPTY: [(i32, f32); 1] = [(0, 0.0); 1];
|
||||
unsafe { EMPTY[0] = (EVENT_LIBRARY_SIZE, 0.0); }
|
||||
return unsafe { &EMPTY[..1] };
|
||||
self.events[0] = (EVENT_LIBRARY_SIZE, 0.0);
|
||||
return &self.events[..1];
|
||||
}
|
||||
let (nearest_id, distance) = self.search(features);
|
||||
self.last_nearest = nearest_id;
|
||||
@@ -205,14 +207,11 @@ impl MicroHnsw {
|
||||
self.nodes[nearest_id].label
|
||||
} else { CLASS_UNKNOWN };
|
||||
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
unsafe {
|
||||
EVENTS[0] = (EVENT_NEAREST_MATCH_ID, nearest_id as f32);
|
||||
EVENTS[1] = (EVENT_MATCH_DISTANCE, distance);
|
||||
EVENTS[2] = (EVENT_CLASSIFICATION, label as f32);
|
||||
EVENTS[3] = (EVENT_LIBRARY_SIZE, self.n_vectors as f32);
|
||||
}
|
||||
unsafe { &EVENTS[..4] }
|
||||
self.events[0] = (EVENT_NEAREST_MATCH_ID, nearest_id as f32);
|
||||
self.events[1] = (EVENT_MATCH_DISTANCE, distance);
|
||||
self.events[2] = (EVENT_CLASSIFICATION, label as f32);
|
||||
self.events[3] = (EVENT_LIBRARY_SIZE, self.n_vectors as f32);
|
||||
&self.events[..4]
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize { self.n_vectors }
|
||||
|
||||
@@ -48,6 +48,8 @@ pub const EVENT_INFLUENCE_CHANGE: i32 = 762;
|
||||
|
||||
/// PageRank influence tracker.
|
||||
pub struct PageRankInfluence {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 8],
|
||||
/// Weighted adjacency matrix (row-major, adj[i][j] = correlation i<->j).
|
||||
adj: [[f32; MAX_PERSONS]; MAX_PERSONS],
|
||||
/// Current PageRank vector.
|
||||
@@ -63,6 +65,7 @@ pub struct PageRankInfluence {
|
||||
impl PageRankInfluence {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 8],
|
||||
adj: [[0.0; MAX_PERSONS]; MAX_PERSONS],
|
||||
rank: [0.25; MAX_PERSONS],
|
||||
prev_rank: [0.25; MAX_PERSONS],
|
||||
@@ -190,9 +193,8 @@ impl PageRankInfluence {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build output events into a static buffer.
|
||||
fn build_events(&self, np: usize) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8];
|
||||
/// Build output events into the owned per-call buffer.
|
||||
fn build_events(&mut self, np: usize) -> &[(i32, f32)] {
|
||||
let mut n = 0usize;
|
||||
|
||||
// Find dominant person.
|
||||
@@ -206,15 +208,11 @@ impl PageRankInfluence {
|
||||
}
|
||||
|
||||
// Emit dominant person every frame.
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_DOMINANT_PERSON, best_idx as f32);
|
||||
}
|
||||
self.events[n] = (EVENT_DOMINANT_PERSON, best_idx as f32);
|
||||
n += 1;
|
||||
|
||||
// Emit influence score every frame.
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_INFLUENCE_SCORE, best_rank);
|
||||
}
|
||||
self.events[n] = (EVENT_INFLUENCE_SCORE, best_rank);
|
||||
n += 1;
|
||||
|
||||
// Emit change events for persons whose rank shifted significantly.
|
||||
@@ -223,14 +221,12 @@ impl PageRankInfluence {
|
||||
if fabsf(delta) > CHANGE_THRESHOLD && n < 8 {
|
||||
// Encode: integer part = person_id, fractional = clamped delta.
|
||||
let encoded = i as f32 + delta.clamp(-0.49, 0.49);
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_INFLUENCE_CHANGE, encoded);
|
||||
}
|
||||
self.events[n] = (EVENT_INFLUENCE_CHANGE, encoded);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Get the current PageRank score for a person.
|
||||
|
||||
@@ -69,6 +69,8 @@ pub const EVENT_TRACK_LOST: i32 = 773;
|
||||
|
||||
/// Spiking neural network person tracker.
|
||||
pub struct SpikingTracker {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Membrane potential of each input neuron.
|
||||
membrane: [f32; N_INPUT],
|
||||
/// Synaptic weights from input to output neurons.
|
||||
@@ -109,6 +111,7 @@ impl SpikingTracker {
|
||||
}
|
||||
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
membrane: [0.0; N_INPUT],
|
||||
weights,
|
||||
input_spike_time: [0; N_INPUT],
|
||||
@@ -242,8 +245,7 @@ impl SpikingTracker {
|
||||
}
|
||||
|
||||
/// Construct event output.
|
||||
fn build_events(&self, zone: i8, was_active: bool) -> &[(i32, f32)] {
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
fn build_events(&mut self, zone: i8, was_active: bool) -> &[(i32, f32)] {
|
||||
let mut n = 0usize;
|
||||
|
||||
// Mean spike rate across all zones.
|
||||
@@ -255,29 +257,29 @@ impl SpikingTracker {
|
||||
|
||||
if zone >= 0 {
|
||||
// TRACK_UPDATE with zone ID.
|
||||
unsafe { EVENTS[n] = (EVENT_TRACK_UPDATE, zone as f32); }
|
||||
self.events[n] = (EVENT_TRACK_UPDATE, zone as f32);
|
||||
n += 1;
|
||||
|
||||
// TRACK_VELOCITY.
|
||||
unsafe { EVENTS[n] = (EVENT_TRACK_VELOCITY, self.velocity_ema); }
|
||||
self.events[n] = (EVENT_TRACK_VELOCITY, self.velocity_ema);
|
||||
n += 1;
|
||||
|
||||
// SPIKE_RATE.
|
||||
unsafe { EVENTS[n] = (EVENT_SPIKE_RATE, mean_rate); }
|
||||
self.events[n] = (EVENT_SPIKE_RATE, mean_rate);
|
||||
n += 1;
|
||||
} else {
|
||||
// SPIKE_RATE even when no track.
|
||||
unsafe { EVENTS[n] = (EVENT_SPIKE_RATE, mean_rate); }
|
||||
self.events[n] = (EVENT_SPIKE_RATE, mean_rate);
|
||||
n += 1;
|
||||
|
||||
// TRACK_LOST if we had a track before.
|
||||
if was_active {
|
||||
unsafe { EVENTS[n] = (EVENT_TRACK_LOST, self.prev_zone as f32); }
|
||||
self.events[n] = (EVENT_TRACK_LOST, self.prev_zone as f32);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Get the current tracked zone (-1 if lost).
|
||||
|
||||
@@ -73,6 +73,8 @@ impl PlanNode {
|
||||
|
||||
/// GOAP autonomy planner.
|
||||
pub struct GoapPlanner {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
world_state: WorldState,
|
||||
current_goal: u8,
|
||||
plan: [u8; MAX_PLAN_DEPTH],
|
||||
@@ -89,6 +91,7 @@ impl GoapPlanner {
|
||||
let mut p = [0.0f32; NUM_GOALS];
|
||||
p[0]=0.9; p[1]=0.8; p[2]=0.7; p[3]=0.5; p[4]=0.3; p[5]=0.1;
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
world_state: 0, current_goal: 0xFF,
|
||||
plan: [0xFF; MAX_PLAN_DEPTH], plan_len: 0, plan_step: 0,
|
||||
goal_priorities: p, timer_count: 0, replan_interval: 60,
|
||||
@@ -112,17 +115,16 @@ impl GoapPlanner {
|
||||
/// Called at ~1 Hz. Replans periodically and executes plan steps.
|
||||
pub fn on_timer(&mut self) -> &[(i32, f32)] {
|
||||
self.timer_count += 1;
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
// Replan at interval.
|
||||
if self.timer_count % self.replan_interval == 0 {
|
||||
let g = self.select_goal();
|
||||
if g < NUM_GOALS as u8 {
|
||||
self.current_goal = g;
|
||||
if n < 4 { unsafe { EVENTS[n] = (EVENT_GOAL_SELECTED, g as f32); } n += 1; }
|
||||
if n < 4 { self.events[n] = (EVENT_GOAL_SELECTED, g as f32); n += 1; }
|
||||
let cost = self.plan_for_goal(g as usize);
|
||||
if cost < 255 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_PLAN_COST, cost as f32); } n += 1;
|
||||
self.events[n] = (EVENT_PLAN_COST, cost as f32); n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,16 +137,16 @@ impl GoapPlanner {
|
||||
let old = self.world_state;
|
||||
self.world_state = action.apply(self.world_state);
|
||||
if (self.world_state & !old) != 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_MODULE_ACTIVATED, aid as f32); } n += 1;
|
||||
self.events[n] = (EVENT_MODULE_ACTIVATED, aid as f32); n += 1;
|
||||
}
|
||||
if (old & !self.world_state) != 0 && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_MODULE_DEACTIVATED, aid as f32); } n += 1;
|
||||
self.events[n] = (EVENT_MODULE_DEACTIVATED, aid as f32); n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.plan_step += 1;
|
||||
}
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
fn select_goal(&self) -> u8 {
|
||||
|
||||
@@ -38,6 +38,8 @@ impl PatternEntry { const fn empty() -> Self { Self { symbols: [0; PATTERN_LEN],
|
||||
|
||||
/// Temporal pattern sequence analyzer.
|
||||
pub struct PatternSequenceAnalyzer {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 4],
|
||||
/// Two-day history: [0..DAY_LEN)=yesterday, [DAY_LEN..2*DAY_LEN)=today.
|
||||
history: [u8; DAY_LEN * 2],
|
||||
minute_counter: u16,
|
||||
@@ -55,6 +57,7 @@ pub struct PatternSequenceAnalyzer {
|
||||
impl PatternSequenceAnalyzer {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 4],
|
||||
history: [0; DAY_LEN * 2], minute_counter: 0, day_offset: 0,
|
||||
pattern_lib: [PatternEntry::empty(); MAX_PATTERNS], n_patterns: 0,
|
||||
routine_confidence: 0.0, frame_votes: [0; 5], frames_in_minute: 0,
|
||||
@@ -72,7 +75,6 @@ impl PatternSequenceAnalyzer {
|
||||
/// Called at ~1 Hz. Commits symbols and runs hourly LCS comparison.
|
||||
pub fn on_timer(&mut self) -> &[(i32, f32)] {
|
||||
self.timer_count += 1;
|
||||
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
|
||||
let mut n = 0usize;
|
||||
|
||||
if self.timer_count % 60 == 0 && self.frames_in_minute > 0 {
|
||||
@@ -83,12 +85,12 @@ impl PatternSequenceAnalyzer {
|
||||
if self.day_offset > 0 {
|
||||
let predicted = self.history[self.minute_counter as usize];
|
||||
if sym as u8 != predicted && n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_ROUTINE_DEVIATION, self.minute_counter as f32); }
|
||||
self.events[n] = (EVENT_ROUTINE_DEVIATION, self.minute_counter as f32);
|
||||
n += 1;
|
||||
}
|
||||
let next_min = (self.minute_counter + 1) % DAY_LEN as u16;
|
||||
if n < 4 {
|
||||
unsafe { EVENTS[n] = (EVENT_PREDICTION_NEXT, self.history[next_min as usize] as f32); }
|
||||
self.events[n] = (EVENT_PREDICTION_NEXT, self.history[next_min as usize] as f32);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -104,14 +106,14 @@ impl PatternSequenceAnalyzer {
|
||||
if wlen >= MIN_PATTERN_LEN {
|
||||
let lcs = self.compute_lcs(start, wlen);
|
||||
self.routine_confidence = if wlen > 0 { lcs as f32 / wlen as f32 } else { 0.0 };
|
||||
if n < 4 { unsafe { EVENTS[n] = (EVENT_PATTERN_CONFIDENCE, self.routine_confidence); } n += 1; }
|
||||
if n < 4 { self.events[n] = (EVENT_PATTERN_CONFIDENCE, self.routine_confidence); n += 1; }
|
||||
if lcs >= MIN_PATTERN_LEN {
|
||||
self.store_pattern(start, wlen);
|
||||
if n < 4 { unsafe { EVENTS[n] = (EVENT_PATTERN_DETECTED, lcs as f32); } n += 1; }
|
||||
if n < 4 { self.events[n] = (EVENT_PATTERN_DETECTED, lcs as f32); n += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
fn majority_symbol(&self) -> Symbol {
|
||||
|
||||
@@ -45,18 +45,19 @@ pub struct TemporalLogicGuard {
|
||||
vio_counts: [u32; NUM_RULES],
|
||||
frame_idx: u32,
|
||||
report_interval: u32,
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 12],
|
||||
}
|
||||
|
||||
impl TemporalLogicGuard {
|
||||
pub const fn new() -> Self {
|
||||
Self { rules: [Rule::new(); NUM_RULES], vio_counts: [0; NUM_RULES],
|
||||
frame_idx: 0, report_interval: 200 }
|
||||
frame_idx: 0, report_interval: 200, events: [(0, 0.0); 12] }
|
||||
}
|
||||
|
||||
/// Process one frame. Returns events to emit.
|
||||
pub fn on_frame(&mut self, input: &FrameInput) -> &[(i32, f32)] {
|
||||
self.frame_idx += 1;
|
||||
static mut EV: [(i32, f32); 12] = [(0, 0.0); 12];
|
||||
let mut n = 0usize;
|
||||
|
||||
// G-rules (0-3, 6): violated when condition holds on any frame.
|
||||
@@ -75,10 +76,10 @@ impl TemporalLogicGuard {
|
||||
self.rules[rid].state = RuleState::Violated;
|
||||
self.rules[rid].vio_frame = self.frame_idx;
|
||||
self.vio_counts[rid] += 1;
|
||||
if n + 1 < 12 { unsafe {
|
||||
EV[n] = (EVENT_LTL_VIOLATION, rid as f32);
|
||||
EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
} n += 2; }
|
||||
if n + 1 < 12 {
|
||||
self.events[n] = (EVENT_LTL_VIOLATION, rid as f32);
|
||||
self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
n += 2; }
|
||||
}
|
||||
} else { self.rules[rid].state = RuleState::Satisfied; }
|
||||
g += 1;
|
||||
@@ -86,18 +87,18 @@ impl TemporalLogicGuard {
|
||||
|
||||
// Rule 4: F(motion_start -> motion_end within 300s).
|
||||
if self.check_deadline_rule(4, input.motion_energy > 0.1, MOTION_STOP_DEADLINE) {
|
||||
if n + 1 < 12 { unsafe {
|
||||
EV[n] = (EVENT_LTL_VIOLATION, 4.0);
|
||||
EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
} n += 2; }
|
||||
if n + 1 < 12 {
|
||||
self.events[n] = (EVENT_LTL_VIOLATION, 4.0);
|
||||
self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
n += 2; }
|
||||
}
|
||||
|
||||
// Rule 5: G(breathing>40 -> alert within 5s).
|
||||
if self.check_deadline_rule(5, input.breathing_bpm > 40.0, FAST_BREATH_DEADLINE) {
|
||||
if n + 1 < 12 { unsafe {
|
||||
EV[n] = (EVENT_LTL_VIOLATION, 5.0);
|
||||
EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
} n += 2; }
|
||||
if n + 1 < 12 {
|
||||
self.events[n] = (EVENT_LTL_VIOLATION, 5.0);
|
||||
self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
n += 2; }
|
||||
}
|
||||
|
||||
// Rule 7: G(seizure -> !normal_gait within 60s).
|
||||
@@ -113,10 +114,10 @@ impl TemporalLogicGuard {
|
||||
self.rules[7].state = RuleState::Violated;
|
||||
self.rules[7].vio_frame = self.frame_idx;
|
||||
self.vio_counts[7] += 1;
|
||||
if n + 1 < 12 { unsafe {
|
||||
EV[n] = (EVENT_LTL_VIOLATION, 7.0);
|
||||
EV[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
} n += 2; }
|
||||
if n + 1 < 12 {
|
||||
self.events[n] = (EVENT_LTL_VIOLATION, 7.0);
|
||||
self.events[n+1] = (EVENT_COUNTEREXAMPLE, self.frame_idx as f32);
|
||||
n += 2; }
|
||||
} else if self.frame_idx >= self.rules[7].deadline {
|
||||
self.rules[7].state = RuleState::Satisfied;
|
||||
}
|
||||
@@ -129,10 +130,10 @@ impl TemporalLogicGuard {
|
||||
}
|
||||
|
||||
if self.frame_idx % self.report_interval == 0 && n < 12 {
|
||||
unsafe { EV[n] = (EVENT_LTL_SATISFACTION, self.satisfied_count() as f32); }
|
||||
self.events[n] = (EVENT_LTL_SATISFACTION, self.satisfied_count() as f32);
|
||||
n += 1;
|
||||
}
|
||||
unsafe { &EV[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Generic deadline rule: condition triggers pending, expiry = violation,
|
||||
|
||||
@@ -137,6 +137,8 @@ impl VitalHistory {
|
||||
|
||||
/// Vital trend analyzer.
|
||||
pub struct VitalTrendAnalyzer {
|
||||
/// Per-call event scratch buffer (owned; replaces former `static mut`).
|
||||
events: [(i32, f32); 8],
|
||||
breathing: VitalHistory,
|
||||
heartrate: VitalHistory,
|
||||
/// Debounce counters for each alert type.
|
||||
@@ -153,6 +155,7 @@ pub struct VitalTrendAnalyzer {
|
||||
impl VitalTrendAnalyzer {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
events: [(0, 0.0); 8],
|
||||
breathing: VitalHistory::new(),
|
||||
heartrate: VitalHistory::new(),
|
||||
bradypnea_count: 0,
|
||||
@@ -172,16 +175,13 @@ impl VitalTrendAnalyzer {
|
||||
self.breathing.push(breathing_bpm);
|
||||
self.heartrate.push(heartrate_bpm);
|
||||
|
||||
static mut EVENTS: [(i32, f32); 8] = [(0, 0.0); 8];
|
||||
let mut n = 0usize;
|
||||
|
||||
// ── Apnea detection (highest priority) ──────────────────────────
|
||||
if breathing_bpm < 1.0 {
|
||||
self.apnea_counter += 1;
|
||||
if self.apnea_counter >= APNEA_SECONDS {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_APNEA, self.apnea_counter as f32);
|
||||
}
|
||||
self.events[n] = (EVENT_APNEA, self.apnea_counter as f32);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -192,9 +192,7 @@ impl VitalTrendAnalyzer {
|
||||
if breathing_bpm > 0.0 && breathing_bpm < BRADYPNEA_THRESH {
|
||||
self.bradypnea_count = self.bradypnea_count.saturating_add(1);
|
||||
if self.bradypnea_count >= ALERT_DEBOUNCE && n < 7 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_BRADYPNEA, breathing_bpm);
|
||||
}
|
||||
self.events[n] = (EVENT_BRADYPNEA, breathing_bpm);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -205,9 +203,7 @@ impl VitalTrendAnalyzer {
|
||||
if breathing_bpm > TACHYPNEA_THRESH {
|
||||
self.tachypnea_count = self.tachypnea_count.saturating_add(1);
|
||||
if self.tachypnea_count >= ALERT_DEBOUNCE && n < 7 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_TACHYPNEA, breathing_bpm);
|
||||
}
|
||||
self.events[n] = (EVENT_TACHYPNEA, breathing_bpm);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -218,9 +214,7 @@ impl VitalTrendAnalyzer {
|
||||
if heartrate_bpm > 0.0 && heartrate_bpm < BRADYCARDIA_THRESH {
|
||||
self.bradycardia_count = self.bradycardia_count.saturating_add(1);
|
||||
if self.bradycardia_count >= ALERT_DEBOUNCE && n < 7 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_BRADYCARDIA, heartrate_bpm);
|
||||
}
|
||||
self.events[n] = (EVENT_BRADYCARDIA, heartrate_bpm);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -231,9 +225,7 @@ impl VitalTrendAnalyzer {
|
||||
if heartrate_bpm > TACHYCARDIA_THRESH {
|
||||
self.tachycardia_count = self.tachycardia_count.saturating_add(1);
|
||||
if self.tachycardia_count >= ALERT_DEBOUNCE && n < 7 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_TACHYCARDIA, heartrate_bpm);
|
||||
}
|
||||
self.events[n] = (EVENT_TACHYCARDIA, heartrate_bpm);
|
||||
n += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -245,20 +237,16 @@ impl VitalTrendAnalyzer {
|
||||
let br_avg = self.breathing.mean_last(WINDOW_1M);
|
||||
let hr_avg = self.heartrate.mean_last(WINDOW_1M);
|
||||
if n < 7 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_BREATHING_AVG, br_avg);
|
||||
}
|
||||
self.events[n] = (EVENT_BREATHING_AVG, br_avg);
|
||||
n += 1;
|
||||
}
|
||||
if n < 8 {
|
||||
unsafe {
|
||||
EVENTS[n] = (EVENT_HEARTRATE_AVG, hr_avg);
|
||||
}
|
||||
self.events[n] = (EVENT_HEARTRATE_AVG, hr_avg);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { &EVENTS[..n] }
|
||||
&self.events[..n]
|
||||
}
|
||||
|
||||
/// Get the 1-minute breathing average.
|
||||
|
||||
Reference in New Issue
Block a user