233 lines
7.0 KiB
C++
233 lines
7.0 KiB
C++
#pragma once
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
namespace AlertFaultPolicy {
|
|
|
|
static const uint32_t kCheckIntervalMs = 5000UL;
|
|
static const uint16_t kMinIntervalMinutes = 60;
|
|
static const uint32_t kMsPerMinute = 60000UL;
|
|
|
|
enum class State : uint8_t { OK, FIRING };
|
|
|
|
struct Fault {
|
|
State state;
|
|
uint32_t fired_at_ms;
|
|
uint32_t last_outage_started_ms;
|
|
};
|
|
|
|
struct OutageSnapshot {
|
|
bool down;
|
|
uint32_t started_ms;
|
|
uint8_t reason;
|
|
};
|
|
|
|
enum class Action : uint8_t { None, FireDown, FireRecovered };
|
|
|
|
struct TickResult {
|
|
Action action;
|
|
uint32_t duration_ms;
|
|
};
|
|
|
|
static inline OutageSnapshot fromStartMs(uint32_t started_ms) {
|
|
OutageSnapshot snapshot{};
|
|
snapshot.down = started_ms != 0;
|
|
snapshot.started_ms = started_ms;
|
|
return snapshot;
|
|
}
|
|
|
|
// Pack the cross-task outage state into one atomic word. The explicit down bit
|
|
// means an outage beginning at millis()==0 remains representable.
|
|
static const uint64_t kOutageDownBit = 1ULL << 40;
|
|
|
|
static inline uint64_t packOutageSnapshot(OutageSnapshot snapshot) {
|
|
if (!snapshot.down) {
|
|
snapshot.started_ms = 0;
|
|
snapshot.reason = 0;
|
|
}
|
|
uint64_t value = (uint64_t)snapshot.started_ms;
|
|
value |= (uint64_t)snapshot.reason << 32;
|
|
if (snapshot.down) value |= kOutageDownBit;
|
|
return value;
|
|
}
|
|
|
|
static inline OutageSnapshot unpackOutageSnapshot(uint64_t value) {
|
|
OutageSnapshot snapshot{};
|
|
snapshot.started_ms = (uint32_t)value;
|
|
snapshot.reason = (uint8_t)(value >> 32);
|
|
snapshot.down = (value & kOutageDownBit) != 0;
|
|
if (!snapshot.down) {
|
|
snapshot.started_ms = 0;
|
|
snapshot.reason = 0;
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
static inline OutageSnapshot applyWifiStatus(uint32_t now, bool connected,
|
|
OutageSnapshot current,
|
|
bool initialized) {
|
|
if (connected) {
|
|
if (!initialized || current.down) return OutageSnapshot{};
|
|
return current;
|
|
}
|
|
if (!initialized || !current.down) {
|
|
OutageSnapshot snapshot{};
|
|
snapshot.down = true;
|
|
snapshot.started_ms = now;
|
|
snapshot.reason = current.reason;
|
|
return snapshot;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
static inline OutageSnapshot applyWifiGotIp(OutageSnapshot) {
|
|
return OutageSnapshot{};
|
|
}
|
|
|
|
// Preserve the first outage start and first useful reason. Reconnect attempts
|
|
// may produce later local disconnect events which must not rewrite either.
|
|
static inline OutageSnapshot applyWifiDisconnectEvent(
|
|
uint32_t now, uint8_t reason, OutageSnapshot current) {
|
|
if (!current.down) {
|
|
OutageSnapshot snapshot{};
|
|
snapshot.down = true;
|
|
snapshot.started_ms = now;
|
|
snapshot.reason = reason;
|
|
return snapshot;
|
|
}
|
|
if (current.reason == 0 && reason != 0) current.reason = reason;
|
|
return current;
|
|
}
|
|
|
|
static inline uint32_t elapsedMs(uint32_t now, uint32_t then) {
|
|
return now - then;
|
|
}
|
|
|
|
static inline bool checkDue(uint32_t now, uint32_t next_check_ms) {
|
|
return (int32_t)(now - next_check_ms) >= 0;
|
|
}
|
|
|
|
static inline uint32_t nextCheckMs(uint32_t now) {
|
|
return now + kCheckIntervalMs;
|
|
}
|
|
|
|
static inline uint32_t minIntervalMs(uint16_t configured_minutes) {
|
|
uint16_t minutes = configured_minutes < kMinIntervalMinutes
|
|
? kMinIntervalMinutes
|
|
: configured_minutes;
|
|
return (uint32_t)minutes * kMsPerMinute;
|
|
}
|
|
|
|
static inline uint32_t thresholdMs(uint16_t minutes) {
|
|
return (uint32_t)minutes * kMsPerMinute;
|
|
}
|
|
|
|
static inline uint32_t downDurationMs(uint32_t now,
|
|
const OutageSnapshot& snapshot) {
|
|
return snapshot.down ? elapsedMs(now, snapshot.started_ms) : 0;
|
|
}
|
|
|
|
static inline bool rateLimitAllows(uint32_t now, uint32_t fired_at_ms,
|
|
uint32_t min_interval_ms) {
|
|
return fired_at_ms == 0 ||
|
|
elapsedMs(now, fired_at_ms) >= min_interval_ms;
|
|
}
|
|
|
|
static inline TickResult tick(const Fault& fault, uint32_t now,
|
|
const OutageSnapshot& snapshot,
|
|
uint32_t threshold_ms,
|
|
uint32_t min_interval_ms) {
|
|
TickResult result = {Action::None, 0};
|
|
if (fault.state == State::OK) {
|
|
const uint32_t down_ms = downDurationMs(now, snapshot);
|
|
if (snapshot.down && down_ms >= threshold_ms &&
|
|
rateLimitAllows(now, fault.fired_at_ms, min_interval_ms)) {
|
|
result.action = Action::FireDown;
|
|
result.duration_ms = down_ms;
|
|
}
|
|
} else if (!snapshot.down) {
|
|
result.action = Action::FireRecovered;
|
|
result.duration_ms = elapsedMs(now, fault.last_outage_started_ms);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static inline void commitDown(Fault& fault, uint32_t now,
|
|
uint32_t outage_start_ms) {
|
|
fault.state = State::FIRING;
|
|
fault.fired_at_ms = now;
|
|
fault.last_outage_started_ms = outage_start_ms;
|
|
}
|
|
|
|
static inline void commitRecovered(Fault& fault) {
|
|
fault.state = State::OK;
|
|
}
|
|
|
|
static inline void reset(Fault& fault) {
|
|
fault.state = State::OK;
|
|
fault.fired_at_ms = 0;
|
|
}
|
|
|
|
static inline void rearmIfDisabled(Fault& fault) {
|
|
if (fault.state == State::FIRING) fault.state = State::OK;
|
|
}
|
|
|
|
static inline void formatAge(uint32_t age_ms, char* out, size_t out_size) {
|
|
if (!out || out_size == 0) return;
|
|
uint32_t seconds = age_ms / 1000U;
|
|
uint32_t hours = seconds / 3600U;
|
|
uint32_t minutes = (seconds % 3600U) / 60U;
|
|
if (hours > 0) {
|
|
snprintf(out, out_size, "%uh%um", (unsigned)hours, (unsigned)minutes);
|
|
} else {
|
|
snprintf(out, out_size, "%um", (unsigned)minutes);
|
|
}
|
|
}
|
|
|
|
static inline bool formatWifiAlert(char* out, size_t out_size,
|
|
const TickResult& result,
|
|
const OutageSnapshot& snapshot) {
|
|
if (!out || out_size == 0) return false;
|
|
char age[16];
|
|
formatAge(result.duration_ms, age, sizeof(age));
|
|
if (result.action == Action::FireDown) {
|
|
if (snapshot.reason != 0) {
|
|
snprintf(out, out_size, "WiFi down %s (reason %u)", age,
|
|
(unsigned)snapshot.reason);
|
|
} else {
|
|
snprintf(out, out_size, "WiFi down %s", age);
|
|
}
|
|
return true;
|
|
}
|
|
if (result.action == Action::FireRecovered) {
|
|
snprintf(out, out_size, "WiFi recovered after %s", age);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static inline void formatMqttDown(char* out, size_t out_size, int slot_1based,
|
|
const char* preset_name,
|
|
uint32_t duration_ms) {
|
|
if (!out || out_size == 0) return;
|
|
char age[16];
|
|
formatAge(duration_ms, age, sizeof(age));
|
|
snprintf(out, out_size, "MQTT slot %d (%s) down %s", slot_1based,
|
|
preset_name ? preset_name : "?", age);
|
|
}
|
|
|
|
static inline void formatMqttRecovered(char* out, size_t out_size,
|
|
int slot_1based,
|
|
const char* preset_name,
|
|
uint32_t duration_ms) {
|
|
if (!out || out_size == 0) return;
|
|
char age[16];
|
|
formatAge(duration_ms, age, sizeof(age));
|
|
snprintf(out, out_size, "MQTT slot %d (%s) recovered after %s", slot_1based,
|
|
preset_name ? preset_name : "?", age);
|
|
}
|
|
|
|
} // namespace AlertFaultPolicy
|