ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
l1_delta_tracker.h
Go to the documentation of this file.
1/*
2 * ESPectre - L1 Delta Tracker
3 *
4 * Tracks L1 amplitude deltas across CSI subcarriers.
5 *
6 * Author: Francesco Pace <francesco.pace@gmail.com>
7 * SPDX-License-Identifier: GPL-3.0-only
8 * Commercial licensing available under separate agreement; see LICENSING.md.
9 */
10#pragma once
11
12#include <algorithm>
13#include <cmath>
14#include <cstdint>
15#include <cstring>
16#include <limits>
17#include <new>
18
19#include "csi_format.h"
20#include "detector_limits.h"
21#include "csi_features.h"
22#include "filters.h"
23#include "utils.h"
24
25namespace espectre {
26
27/**
28 * One sliding window of displacements over a caller-owned ring.
29 *
30 * The tracker keeps two of these, one per lag, and they share a single
31 * allocation, so the pair costs no extra bookkeeping and no second heap block.
32 */
34 float* ring{nullptr};
35 uint16_t index{0U};
36 uint16_t slots{0U};
37 uint16_t count{0U};
38 float sum{0.0f};
39
40 void push(float value, uint16_t capacity) {
41 if (capacity == 0U || ring == nullptr) {
42 return;
43 }
44 if (slots >= capacity && std::isfinite(ring[index])) {
45 sum -= ring[index];
46 --count;
47 }
48 ring[index] = value;
49 if (std::isfinite(value)) {
50 sum += value;
51 ++count;
52 }
53 index++;
54 if (index >= capacity) {
55 index = 0U;
56 }
57 if (slots < capacity) ++slots;
58 }
59
60 float mean() const { return count > 0U ? sum / static_cast<float>(count) : 0.0f; }
61
62 void clear(uint16_t capacity) {
63 index = 0U;
64 slots = 0U;
65 count = 0U;
66 sum = 0.0f;
67 if (ring != nullptr && capacity > 0U) {
68 std::memset(ring, 0, capacity * sizeof(float));
69 }
70 }
71};
72
74 public:
75 L1DeltaTracker() = default;
76 ~L1DeltaTracker() { delete[] storage_; }
78 : capacity_(other.capacity_),
79 lag_(other.lag_),
80 profile_index_(other.profile_index_),
81 storage_(other.storage_),
82 lagged_(other.lagged_),
83 adjacent_(other.adjacent_),
84 hampel_state_(other.hampel_state_),
85 hampel_adjacent_(other.hampel_adjacent_) {
86 std::memcpy(profile_ring_, other.profile_ring_, sizeof(profile_ring_));
87 std::memcpy(profile_len_, other.profile_len_, sizeof(profile_len_));
88 other.capacity_ = 0U;
89 other.profile_index_ = 0U;
90 other.storage_ = nullptr;
91 other.lagged_ = L1DeltaWindow{};
92 other.adjacent_ = L1DeltaWindow{};
93 }
95 if (this != &other) {
96 delete[] storage_;
97 capacity_ = other.capacity_;
98 std::memcpy(profile_ring_, other.profile_ring_, sizeof(profile_ring_));
99 std::memcpy(profile_len_, other.profile_len_, sizeof(profile_len_));
100 lag_ = other.lag_;
101 profile_index_ = other.profile_index_;
102 storage_ = other.storage_;
103 lagged_ = other.lagged_;
104 adjacent_ = other.adjacent_;
105 hampel_state_ = other.hampel_state_;
106 hampel_adjacent_ = other.hampel_adjacent_;
107 other.capacity_ = 0U;
108 other.profile_index_ = 0U;
109 other.storage_ = nullptr;
110 other.lagged_ = L1DeltaWindow{};
111 other.adjacent_ = L1DeltaWindow{};
112 }
113 return *this;
114 }
117
118 /**
119 * @param capacity Delta ring capacity in packets
120 * @param lag Profile-displacement distance in packets, bounded by
121 * L1_DELTA_LAG_MAX because the profile ring is statically sized
122 */
123 void configure(uint16_t capacity, uint16_t lag = L1_DELTA_LAG) {
124 allocate_delta_ring_(std::min<uint16_t>(capacity, DETECTOR_MAX_WINDOW_SIZE));
125 lag_ = std::min<uint16_t>(lag > 0U ? lag : 1U, L1_DELTA_LAG_MAX);
126 clear();
127 }
128
129 void configure_hampel(bool enabled,
130 uint8_t window_size = HAMPEL_TURBULENCE_WINDOW_DEFAULT,
131 float threshold = HAMPEL_TURBULENCE_THRESHOLD_DEFAULT) {
132 hampel_turbulence_init(&hampel_state_, window_size, threshold, enabled);
133 // The ratio divides one displacement by the other, so both must be filtered
134 // alike: an outlier surviving only in the denominator would depress the
135 // ratio and read as less motion.
136 hampel_turbulence_init(&hampel_adjacent_, window_size, threshold, enabled);
137 }
138
139 void clear() {
140 std::memset(profile_ring_, 0, sizeof(profile_ring_));
141 std::memset(profile_len_, 0, sizeof(profile_len_));
142 lagged_.clear(capacity_);
143 adjacent_.clear(capacity_);
144 profile_index_ = 0U;
145 if (hampel_state_.window_size >= HAMPEL_TURBULENCE_WINDOW_MIN) {
146 hampel_turbulence_init(&hampel_adjacent_, hampel_state_.window_size,
147 hampel_state_.threshold, hampel_state_.enabled);
148 hampel_turbulence_init(&hampel_state_, hampel_state_.window_size,
149 hampel_state_.threshold, hampel_state_.enabled);
150 }
151 }
152
153 void process(const float *amplitudes, uint8_t amplitude_count) {
154 const float mean =
155 amplitudes != nullptr && amplitude_count >= 2U
156 ? calculate_mean(amplitudes, amplitude_count)
157 : 0.0f;
158 process(amplitudes, amplitude_count, mean);
159 }
160
161 void process(const float *amplitudes, uint8_t amplitude_count,
162 float amplitude_mean) {
163 if (capacity_ == 0U) {
164 return;
165 }
166
167 float profile[HT20_SELECTED_BAND_SIZE]{};
168 const float *reference = profile_ring_[profile_index_];
169 const uint8_t reference_len = profile_len_[profile_index_];
170 // The packet before this one sits in the slot behind the lagged reference,
171 // so the adjacent displacement needs no storage of its own.
172 const uint16_t previous_index =
173 profile_index_ > 0U ? static_cast<uint16_t>(profile_index_ - 1U)
174 : static_cast<uint16_t>(lag_ - 1U);
175 const float *previous = profile_ring_[previous_index];
176 const uint8_t previous_len = profile_len_[previous_index];
177 uint8_t profile_len = 0U;
178 float lagged_value = std::numeric_limits<float>::quiet_NaN();
179 float adjacent_value = std::numeric_limits<float>::quiet_NaN();
180
181 if (amplitudes != nullptr && amplitude_count >= 2U &&
182 amplitude_count <= HT20_SELECTED_BAND_SIZE) {
183 profile_len = normalize_amplitude_profile(
184 amplitudes, amplitude_count, amplitude_mean, profile);
185 const bool lagged = profile_len > 0U && reference_len == profile_len;
186 const bool adjacent = profile_len > 0U && previous_len == profile_len;
187 if (lagged || adjacent) {
188 // One pass over the normalized profile feeds both displacements.
189 float lagged_sum = 0.0f;
190 float adjacent_sum = 0.0f;
191 for (uint8_t i = 0U; i < profile_len; i++) {
192 if (lagged) {
193 lagged_sum += std::fabs(profile[i] - reference[i]);
194 }
195 if (adjacent) {
196 adjacent_sum += std::fabs(profile[i] - previous[i]);
197 }
198 }
199 if (lagged) {
200 lagged_value = hampel_filter_turbulence(
201 &hampel_state_, lagged_sum / profile_len);
202 }
203 if (adjacent) {
204 adjacent_value = hampel_filter_turbulence(
205 &hampel_adjacent_, adjacent_sum / profile_len);
206 }
207 }
208 }
209
210 std::memcpy(profile_ring_[profile_index_], profile, profile_len * sizeof(float));
211 profile_len_[profile_index_] = profile_len;
212 profile_index_++;
213 if (profile_index_ >= lag_) {
214 profile_index_ = 0U;
215 }
216 lagged_.push(lagged_value, capacity_);
217 adjacent_.push(adjacent_value, capacity_);
218 }
219
221 const float missing = std::numeric_limits<float>::quiet_NaN();
222 for (uint32_t slot = 0U; slot < count; ++slot) {
223 profile_len_[profile_index_] = 0U;
224 profile_index_++;
225 if (profile_index_ >= lag_) profile_index_ = 0U;
226 lagged_.push(missing, capacity_);
227 adjacent_.push(missing, capacity_);
228 }
229 }
230
231 uint16_t count() const { return lagged_.count; }
232 float mean() const { return lagged_.mean(); }
233
234 /**
235 * Mean lagged displacement over mean adjacent displacement.
236 *
237 * Noise saturates the displacement immediately, so its ratio sits near 1.0;
238 * real channel evolution keeps growing with the lag and lifts it. Both terms
239 * share the same units, so the ratio drops the noise floor that makes the raw
240 * mean unusable when the link is weak.
241 */
242 float delta_lag_ratio() const {
243 const float adjacent_mean = adjacent_.mean();
244 if (lagged_.count == 0U || adjacent_.count == 0U || adjacent_mean <= 0.0f) {
245 return 1.0f;
246 }
247 return lagged_.mean() / adjacent_mean;
248 }
249
250 uint16_t build_series(float *out) const {
251 if (out == nullptr || capacity_ == 0U || lagged_.count == 0U) {
252 return 0U;
253 }
254 const uint16_t slots = lagged_.slots;
255 uint16_t source = slots < capacity_ ? 0U : lagged_.index;
256 uint16_t written = 0U;
257 for (uint16_t offset = 0U; offset < slots; ++offset) {
258 const float value = lagged_.ring[source];
259 if (std::isfinite(value)) out[written++] = value;
260 source++;
261 if (source >= capacity_) source = 0U;
262 }
263 return written;
264 }
265
266 private:
267 void allocate_delta_ring_(uint16_t capacity) {
268 if (capacity == capacity_ && (capacity == 0U || storage_ != nullptr)) {
269 return;
270 }
271 delete[] storage_;
272 storage_ = nullptr;
273 capacity_ = 0U;
274 lagged_ = L1DeltaWindow{};
275 adjacent_ = L1DeltaWindow{};
276 if (capacity == 0U) {
277 return;
278 }
279 // One block, two views: the lagged window first, the adjacent one behind it.
280 float* block = new (std::nothrow) float[2U * static_cast<size_t>(capacity)];
281 if (block == nullptr) {
282 return;
283 }
284 storage_ = block;
285 capacity_ = capacity;
286 lagged_.ring = block;
287 adjacent_.ring = block + capacity;
288 }
289
290 uint16_t capacity_{0U};
291 float profile_ring_[L1_DELTA_LAG_MAX][HT20_SELECTED_BAND_SIZE]{};
292 uint8_t profile_len_[L1_DELTA_LAG_MAX]{};
293 uint16_t lag_{L1_DELTA_LAG};
294 uint16_t profile_index_{0U};
295 float* storage_{nullptr};
296 L1DeltaWindow lagged_{};
297 L1DeltaWindow adjacent_{};
298 hampel_filter_state_t hampel_state_{};
299 hampel_filter_state_t hampel_adjacent_{};
300};
301
302} // namespace espectre
L1DeltaTracker(const L1DeltaTracker &)=delete
void configure(uint16_t capacity, uint16_t lag=L1_DELTA_LAG)
uint16_t build_series(float *out) const
void process(const float *amplitudes, uint8_t amplitude_count)
L1DeltaTracker(L1DeltaTracker &&other) noexcept
L1DeltaTracker & operator=(L1DeltaTracker &&other) noexcept
void configure_hampel(bool enabled, uint8_t window_size=HAMPEL_TURBULENCE_WINDOW_DEFAULT, float threshold=HAMPEL_TURBULENCE_THRESHOLD_DEFAULT)
float delta_lag_ratio() const
Mean lagged displacement over mean adjacent displacement.
void advance_missing_slots(uint32_t count)
L1DeltaTracker & operator=(const L1DeltaTracker &)=delete
void process(const float *amplitudes, uint8_t amplitude_count, float amplitude_mean)
constexpr uint8_t L1_DELTA_LAG
float hampel_filter_turbulence(hampel_turbulence_state_t *state, float turbulence)
constexpr uint8_t HAMPEL_TURBULENCE_WINDOW_MIN
constexpr uint8_t HAMPEL_TURBULENCE_WINDOW_DEFAULT
constexpr uint8_t HT20_SELECTED_BAND_SIZE
Definition csi_types.h:27
constexpr uint16_t L1_DELTA_LAG_MAX
float calculate_mean(const float *values, size_t n)
Calculate mean of an array.
Definition utils.h:31
constexpr float HAMPEL_TURBULENCE_THRESHOLD_DEFAULT
hampel_turbulence_state_t hampel_filter_state_t
Definition filters.h:52
uint8_t normalize_amplitude_profile(const float *amplitudes, uint8_t count, float mean, float *out)
Write the mean-normalized amplitude profile into out.
Definition utils.h:170
void hampel_turbulence_init(hampel_turbulence_state_t *state, uint8_t window_size, float threshold, bool enabled)
constexpr uint16_t DETECTOR_MAX_WINDOW_SIZE
One sliding window of displacements over a caller-owned ring.
void clear(uint16_t capacity)
void push(float value, uint16_t capacity)