- 01Valid sample
- 02Rearm below 1.2 g
- 03Cross 1.8 g
- 04300 ms cooldown
Keep the event contract in one class
The counter stores whether it is armed, the last counted time and its current count. It does not read hardware or update a screen.
#pragma once
#include <cmath>
#include <cstdint>
#include <limits>
class MotionCounter {
bool armed_ = true, counted_ = false;
std::uint32_t last_ = 0, count_ = 0;
public:
std::uint32_t count() const { return count_; }
bool sample(std::uint32_t now, float x, float y, float z) {
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) return false;
const float magnitude2 = x*x + y*y + z*z;
if (!std::isfinite(magnitude2)) return false;
if (magnitude2 < 1.44f) armed_ = true;
if (magnitude2 < 3.24f || !armed_) return false;
armed_ = false;
if (counted_ && static_cast<std::uint32_t>(now - last_) < 300) return false;
if (count_ == std::numeric_limits<std::uint32_t>::max()) return false;
++count_;
counted_ = true;
last_ = now;
return true;
}
};Squared magnitude avoids a square root without changing the threshold comparison. Non-finite inputs are ignored before arithmetic can create an event. A sustained high signal disarms the counter once; elapsed time alone does not rearm it. The signal must first return below the lower threshold.
A burst during the cooldown is rejected and still disarms the counter. That makes the next accepted event require a fresh return to the rearm band. The unsigned timestamp difference handles ordinary 32-bit clock rollover when the loop runs frequently enough.
Trace the state
Feed rest, burst, held burst, rest and another burst separated by more than 300 ms. Expect two counts, not three. Repeat with the second burst only 30 ms later and explain its rejection.
Try it
Run the tests, then add a sequence that rotates the acceleration vector while keeping its magnitude unchanged. Preserve the distinction between the sensor's g units and metres per second squared; a unit mistake would change every threshold decision.