Files
ZephCore/zephcore/helpers/OffgridPolicy.h
T
2026-10-12 10:20:40 +02:00

133 lines
4.0 KiB
C++

/*
* SPDX-License-Identifier: MIT
* ZephCore OffgridPolicy - companion off-grid mode (upstream "client repeat")
*
* The frequencies a companion may forward on, and the prefs transitions behind
* the app command, the device toggle and the CLI. Pure decision logic, no I/O.
* Design: devdocs/lld/06-companion.md.
*/
#pragma once
#include <stdint.h>
#include "NodePrefs.h"
namespace mesh {
struct OffgridFreqRange {
uint32_t lower_khz, upper_khz;
};
/* Upstream's allowed client-repeat frequencies; CMD_GET_ALLOWED_REPEAT_FREQ
* reports this table to the app. */
static const OffgridFreqRange OFFGRID_FREQ_RANGES[] = {
{ 433000, 433000 }, /* 433 MHz ISM */
{ 869495, 869495 }, /* EU 869.4-869.65 MHz */
{ 918000, 918000 }, /* US 902-928 MHz */
};
#define OFFGRID_FREQ_RANGE_COUNT (sizeof(mesh::OFFGRID_FREQ_RANGES) / sizeof(mesh::OFFGRID_FREQ_RANGES[0]))
/* Top of the sub-GHz LoRa range; above it no off-grid frequency applies. */
#define OFFGRID_SUBGHZ_MAX_MHZ 960.0f
inline uint32_t offgridFreqKhz(float mhz)
{
return mhz > 0.0f ? (uint32_t)(mhz * 1000.0f + 0.5f) : 0;
}
inline bool isOffgridFreqKhz(uint32_t khz)
{
for (const OffgridFreqRange &r : OFFGRID_FREQ_RANGES) {
if (khz >= r.lower_khz && khz <= r.upper_khz) return true;
}
return false;
}
inline bool isOffgridFreq(float mhz) { return isOffgridFreqKhz(offgridFreqKhz(mhz)); }
/* The off-grid frequency (kHz) nearest to `mhz`; 0 above the sub-GHz range. */
inline uint32_t nearestOffgridFreqKhz(float mhz)
{
if (!(mhz > 0.0f && mhz <= OFFGRID_SUBGHZ_MAX_MHZ)) return 0;
uint32_t khz = offgridFreqKhz(mhz);
uint32_t best = 0, best_dist = UINT32_MAX;
for (const OffgridFreqRange &r : OFFGRID_FREQ_RANGES) {
uint32_t c = khz < r.lower_khz ? r.lower_khz : (khz > r.upper_khz ? r.upper_khz : khz);
uint32_t dist = c > khz ? c - khz : khz - c;
if (dist < best_dist) {
best_dist = dist;
best = c;
}
}
return best;
}
/* Off-grid mode is on: repeat is set AND the node is on an off-grid frequency. */
inline bool offgridActive(const NodePrefs &p)
{
return p.client_repeat != 0 && isOffgridFreq(p.freq);
}
/* Drops a repeat flag left on any other frequency, and the remembered home
* frequency with it (it only means something while off-grid mode is on).
* True if prefs changed. */
inline bool offgridNormalize(NodePrefs &p)
{
if (offgridActive(p)) return false;
bool changed = p.client_repeat != 0 || p.offgrid_home_freq != 0.0f;
p.client_repeat = 0;
p.offgrid_home_freq = 0.0f;
return changed;
}
/* The frequency was set by the app or the CLI: whoever set it owns it, so the
* remembered home frequency goes. */
inline void offgridFreqEdited(NodePrefs &p)
{
p.offgrid_home_freq = 0.0f;
offgridNormalize(p);
}
enum OffgridResult : uint8_t {
OFFGRID_UNCHANGED, /* already in the requested state */
OFFGRID_SET, /* repeat flag changed, frequency kept */
OFFGRID_RETUNED, /* repeat flag and frequency changed: reconfigure the radio */
OFFGRID_NO_BAND, /* refused: no off-grid frequency for the current band */
};
/* The device toggle and `set repeat`. On: move to the nearest off-grid
* frequency and remember the one left. Off: return to it. A node already on
* an off-grid frequency stays there both ways. BW, SF, CR and TX power are
* never touched (as the app). */
inline OffgridResult offgridSet(NodePrefs &p, bool on)
{
if (on) {
if (offgridActive(p)) return OFFGRID_UNCHANGED;
if (isOffgridFreq(p.freq)) {
p.client_repeat = 1;
p.offgrid_home_freq = 0.0f;
return OFFGRID_SET;
}
uint32_t khz = nearestOffgridFreqKhz(p.freq);
if (khz == 0) return OFFGRID_NO_BAND;
p.offgrid_home_freq = p.freq;
p.freq = (float)khz / 1000.0f;
p.client_repeat = 1;
return OFFGRID_RETUNED;
}
bool was_set = p.client_repeat != 0;
float home = offgridActive(p) ? p.offgrid_home_freq : 0.0f;
p.client_repeat = 0;
p.offgrid_home_freq = 0.0f;
if (home != 0.0f) {
p.freq = home;
return OFFGRID_RETUNED;
}
return was_set ? OFFGRID_SET : OFFGRID_UNCHANGED;
}
} /* namespace mesh */