mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-10 15:43:36 -04:00
* build(nrf52): drop unused TinyUSB classes and assert function-name strings Only the CDC class is used on nRF52. Disable the MSC, HID, MIDI, vendor and video class drivers in the Adafruit TinyUSB config, and pass an empty __ASSERT_FUNC so assert() no longer embeds __PRETTY_FUNCTION__ strings. File and line are still reported. rak4631 estimate: ~7.4 KB flash, ~2.3 KB RAM. * fix(nrf52): link only the secp256r1 cc310 curve domain CRYS_ECPKI_GetEcDomain indexes ecDomainsFuncP, which references the parameter tables of all eleven cc310 curves. Bluefruit LESC pairing only requests secp256r1, so override the lookup to return that domain alone. rak4631 estimate: ~7.4 KB flash. * fix(airtime): replace powf in the channel-utilization EMA fold foldChannelUtil was the only powf caller on nRF52. The exponent is an integer step count, so raise the EMA factor by squaring instead; a multi-day sleep still folds in at most 32 multiplications. rak4631 estimate: ~1.9 KB flash. * fix(graphics): use double sin/cos in the compass renderers The compass renderers were the only sinf/cosf callers on nRF52 screen builds, pulling in the float trig kernels next to the double ones GeoCoord already links. Call the double variants instead. rak4631 estimate: ~3.2 KB flash. * fix(motion): use double atan2 for magnetometer heading fallbacks MMC5983MA, QMC6309 and the InkHUD map centre were the remaining application atan2f callers. The double atan2 is already linked, so the float variant only added atan2f, __ieee754_atan2f and atanf. The saving lands once meshtastic/Fusion#1 removes the library's atan2f as well. rak4631 estimate: ~0.8 KB flash with Fusion#1. * fix(hopscale): trim diagnostic logging to state changes and anomalies Drop the save/restore confirmations, the hourly histogram and trend dumps, the denominator step logs and the per-packet hop_limit log (printPacket already reports HopLim). Keep the save-failure and histogram-full warnings, the congestion on/off transition and a single periodic status line, and remove lastScaledPerHop, which only fed the logs. * fix(hopscale): silence cppcheck uselessAssignmentArg on restored count * perf(crypto): use full-schedule AES128/AES256 for AES-CCM aesSetKey used AESSmall128/AESSmall256, which re-derive round keys for every block. AES128/AES256 precompute the schedule, encrypt faster and are already linked by encryptAESCtr, so the AESSmall*/AESTiny* code drops out. No change on ESP32, where AESSmall* already aliases AES128/AES256. rak4631 estimate: ~3.9 KB flash; cipher object up to 184 bytes larger. * perf(nrf52): use the shared software CTR for AES-256 and remove tiny-aes CryptoCell only accelerates AES-128, which stays on hardware. AES-256 CTR now calls CryptoEngine::encryptAESCtr (rweather CTR<AES256>, already linked) instead of the in-tree tiny-aes copy, whose sources were removed in the previous commit. Output is identical. rak4631 estimate: ~0.8 KB flash. * perf(mesh): use std::map for pending retransmissions and API port timestamps NextHopRouter::pending and PhoneAPI::lastPortNumToRadio were the only unordered_map instances linked on nRF52. Switching them to std::map, which is already linked, drops the libstdc++ hashtable, rehash policy and prime table. GlobalPacketId gains operator<; the unused hash functor is removed. rak4631 estimate: ~2.1 KB flash. * perf: parse sensor decimals without strtod The WS85 serial parser (strtof) and DFRobotLarkSensor (String::toFloat) were the only callers of newlib's strtod. Add parseDecimalFloat to meshUtils for plain [+-]digits[.digits] fields and use it at both sites. Covered by test_type_conversions against strtof. rak4631 estimate: ~4.5 KB flash. * perf(gps): compute tan from sin/cos in UTM and OSGR conversion latLongToUTM and latLongToOSGR were the only tan callers. sin and cos are already linked, so deriving tan from them drops tan and __kernel_tan. rak4631 estimate: ~1.1 KB flash. * fix(graphics): only dispatch the theme menu when TFT coloring is enabled The Theme option is only offered with GRAPHICS_TFT_COLORING_ENABLED, but handleMenuSwitch dispatched ThemeMenu unconditionally, linking kThemes and the theme accessors into monochrome builds where the menu is unreachable. rak4631 estimate: ~1 KB flash. * fix(senxx): trim diagnostic logging to errors and user-visible actions Keep all errors and warnings and a single version line; shorten the admin action messages; drop progress chatter, state save/restore confirmations and the per-reading and VOC-state debug dumps. The nested VOC restore branch collapses to one condition with the same behaviour. rak4631 estimate: ~2 KB flash. * build(nrf52): define CRYPTO_AES_NO_DECRYPT CTR and CCM only encrypt, so the AES inverse tables and round helpers are dead code on nRF52. Takes effect once the Crypto dependency includes meshtastic/Crypto#5. rak4631 estimate: ~1.0 KB flash.
364 lines
12 KiB
C++
364 lines
12 KiB
C++
#include "airtime.h"
|
|
#include "NodeDB.h"
|
|
#include "UptimeClock.h"
|
|
#include "configuration.h"
|
|
#include <algorithm>
|
|
#include <assert.h>
|
|
#include <cmath>
|
|
#include <string.h>
|
|
|
|
AirTime *airTime = NULL;
|
|
|
|
AirTime *AirTime::Held::armReentryCheck(AirTime *a)
|
|
{
|
|
#ifdef AIRTIME_REENTRY_CHECK
|
|
// Before the lock: a nested take blocks forever, so a later check would never run.
|
|
assert(!a->reentryFlag);
|
|
a->reentryFlag = true;
|
|
#endif
|
|
return a;
|
|
}
|
|
|
|
AirTime::Held::~Held()
|
|
{
|
|
#ifdef AIRTIME_REENTRY_CHECK
|
|
owner->reentryFlag = false;
|
|
#else
|
|
(void)owner;
|
|
#endif
|
|
}
|
|
|
|
// --- the lock-free core -------------------------------------------------------------------------
|
|
// Every method here requires the lock, and says so in its signature. None can take it: Windows has
|
|
// no lock to reach.
|
|
|
|
void AirTime::Windows::logAirtime(reportTypes reportType, uint32_t airtime_ms, const Held &held)
|
|
{
|
|
// A packet may be logged immediately after waking from light sleep. Sync first so
|
|
// the packet is counted in the current wall-time bucket, not a stale awake-time bucket.
|
|
syncNow(held);
|
|
|
|
// The caller logs, once the lock is released.
|
|
if (reportType == TX_LOG) {
|
|
this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms;
|
|
this->utilizationTX[this->getPeriodUtilHour(held)] += airtime_ms;
|
|
} else if (reportType == RX_LOG) {
|
|
this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms;
|
|
} else if (reportType == RX_ALL_LOG) {
|
|
this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms;
|
|
}
|
|
|
|
// Log all airtime type for channel utilization
|
|
this->channelUtilization[this->getPeriodUtilMinute(held)] += airtime_ms;
|
|
}
|
|
|
|
uint8_t AirTime::Windows::getPeriodUtilMinute(const Held &)
|
|
{
|
|
return (secSinceBoot / 10) % CHANNEL_UTILIZATION_PERIODS;
|
|
}
|
|
|
|
uint8_t AirTime::Windows::getPeriodUtilHour(const Held &)
|
|
{
|
|
return (secSinceBoot / 60) % MINUTES_IN_HOUR;
|
|
}
|
|
|
|
void AirTime::Windows::syncNow(const Held &held)
|
|
{
|
|
// Monotonic uptime, not RTC/network time: a user, GPS, or NTP clock change must not move
|
|
// airtime accounting. Pure read; the main loop publishes the wrap carry it derives from.
|
|
uint32_t nowSecs = Time::getUptimeSecs();
|
|
|
|
if (firstTime) {
|
|
memset(this->utilizationTX, 0, sizeof(this->utilizationTX));
|
|
memset(this->channelUtilization, 0, sizeof(this->channelUtilization));
|
|
memset(this->airtimes.periodTX, 0, sizeof(this->airtimes.periodTX));
|
|
memset(this->airtimes.periodRX, 0, sizeof(this->airtimes.periodRX));
|
|
memset(this->airtimes.periodRX_ALL, 0, sizeof(this->airtimes.periodRX_ALL));
|
|
|
|
this->secSinceBoot = nowSecs;
|
|
firstTime = false;
|
|
return;
|
|
}
|
|
|
|
if (nowSecs == this->secSinceBoot) {
|
|
return;
|
|
}
|
|
|
|
uint32_t oldSecSinceBoot = this->secSinceBoot;
|
|
this->secSinceBoot = nowSecs;
|
|
|
|
// Historical airtime reports use 1-hour buckets. If multiple hours elapsed while
|
|
// asleep, rotate each crossed bucket or clear the whole report window.
|
|
uint32_t elapsedAirtimePeriods = (this->secSinceBoot / SECONDS_PER_PERIOD) - (oldSecSinceBoot / SECONDS_PER_PERIOD);
|
|
if (elapsedAirtimePeriods >= PERIODS_TO_LOG) {
|
|
memset(this->airtimes.periodTX, 0, sizeof(this->airtimes.periodTX));
|
|
memset(this->airtimes.periodRX, 0, sizeof(this->airtimes.periodRX));
|
|
memset(this->airtimes.periodRX_ALL, 0, sizeof(this->airtimes.periodRX_ALL));
|
|
} else {
|
|
// Hand the count to runOnce() rather than tracing each crossing here: this runs under
|
|
// the lock, and a UART write would stall every other caller waiting on it.
|
|
this->rotationsPendingLog += elapsedAirtimePeriods;
|
|
for (uint32_t h = 0; h < elapsedAirtimePeriods; h++) {
|
|
for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) {
|
|
this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i];
|
|
this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i];
|
|
this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i];
|
|
}
|
|
|
|
this->airtimes.periodTX[0] = 0;
|
|
this->airtimes.periodRX[0] = 0;
|
|
this->airtimes.periodRX_ALL[0] = 0;
|
|
}
|
|
}
|
|
|
|
// Channel utilization is a rolling 60-second view split into six 10-second buckets.
|
|
// Clear every bucket crossed while asleep so old airtime decays by real elapsed time.
|
|
uint32_t elapsedUtilPeriods = (this->secSinceBoot / 10) - (oldSecSinceBoot / 10);
|
|
// Fold one reading per crossed bucket, each before that bucket is cleared, so one delayed sync
|
|
// lands where the same number of 10 s syncs would have. Bounded: six clears empty the window.
|
|
const uint32_t steppedUtilPeriods = std::min<uint32_t>(elapsedUtilPeriods, CHANNEL_UTILIZATION_PERIODS);
|
|
for (uint32_t i = 1; i <= steppedUtilPeriods; i++) {
|
|
foldChannelUtil(channelUtilizationPercentRaw(held), 1, held);
|
|
this->channelUtilization[((oldSecSinceBoot / 10) + i) % CHANNEL_UTILIZATION_PERIODS] = 0;
|
|
}
|
|
// Anything past a full window is elapsed time against an already-empty ring, so it folds as
|
|
// idle in closed form rather than looping over a sleep that may have lasted days.
|
|
foldChannelUtil(0.0f, elapsedUtilPeriods - steppedUtilPeriods, held);
|
|
|
|
// TX utilization is a rolling 60-minute view used by duty-cycle checks.
|
|
uint32_t elapsedUtilTXPeriods = (this->secSinceBoot / 60) - (oldSecSinceBoot / 60);
|
|
if (elapsedUtilTXPeriods >= MINUTES_IN_HOUR) {
|
|
memset(this->utilizationTX, 0, sizeof(this->utilizationTX));
|
|
} else {
|
|
for (uint32_t i = 1; i <= elapsedUtilTXPeriods; i++) {
|
|
this->utilizationTX[((oldSecSinceBoot / 60) + i) % MINUTES_IN_HOUR] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AirTime::Windows::airtimeReport(reportTypes reportType, uint32_t *out, size_t count, const Held &held)
|
|
{
|
|
if (!out || count > PERIODS_TO_LOG)
|
|
return false;
|
|
|
|
// Reports may be requested before runOnce() executes after wake.
|
|
syncNow(held);
|
|
|
|
const uint32_t *src = nullptr;
|
|
if (reportType == TX_LOG) {
|
|
src = this->airtimes.periodTX;
|
|
} else if (reportType == RX_LOG) {
|
|
src = this->airtimes.periodRX;
|
|
} else if (reportType == RX_ALL_LOG) {
|
|
src = this->airtimes.periodRX_ALL;
|
|
}
|
|
if (!src)
|
|
return false;
|
|
|
|
memcpy(out, src, count * sizeof(*out));
|
|
return true;
|
|
}
|
|
|
|
float AirTime::Windows::channelUtilizationPercentRaw(const Held &)
|
|
{
|
|
uint32_t sum = 0;
|
|
for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) {
|
|
sum += this->channelUtilization[i];
|
|
}
|
|
|
|
return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100;
|
|
}
|
|
|
|
float AirTime::Windows::channelUtilizationPercent(const Held &held)
|
|
{
|
|
// Gate decisions should see buckets that have decayed across light-sleep time.
|
|
syncNow(held);
|
|
|
|
return channelUtilizationPercentRaw(held);
|
|
}
|
|
|
|
void AirTime::Windows::foldChannelUtil(float sample, uint32_t steps, const Held &)
|
|
{
|
|
if (steps == 0)
|
|
return;
|
|
|
|
if (!hasChannelUtilSample) {
|
|
// Seed from the first reading, or a node booting onto a busy channel reports it quiet
|
|
// for a whole time constant.
|
|
channelUtilAvg = sample;
|
|
hasChannelUtilSample = true;
|
|
steps--;
|
|
}
|
|
|
|
if (steps > 0) {
|
|
// Integer power by squaring; powf would link ~1.9 KB of float libm for this one call.
|
|
float base = 1.0f - 1.0f / float(CHANNEL_UTILIZATION_EMA_DIVISOR);
|
|
float retained = 1.0f;
|
|
for (uint32_t e = steps; e != 0; e >>= 1) {
|
|
if (e & 1)
|
|
retained *= base;
|
|
base *= base;
|
|
}
|
|
channelUtilAvg = sample + (channelUtilAvg - sample) * retained;
|
|
}
|
|
}
|
|
|
|
float AirTime::Windows::smoothedChannelUtilizationPercent(const Held &held)
|
|
{
|
|
syncNow(held);
|
|
|
|
// Nothing folded yet before the first bucket crossing, and 0 would read as an idle channel
|
|
// rather than as no data.
|
|
return hasChannelUtilSample ? channelUtilAvg : channelUtilizationPercentRaw(held);
|
|
}
|
|
|
|
float AirTime::Windows::utilizationTXPercent(const Held &held)
|
|
{
|
|
// Duty-cycle checks use this value, so keep it current even outside the periodic thread.
|
|
syncNow(held);
|
|
|
|
uint32_t sum = 0;
|
|
for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) {
|
|
sum += this->utilizationTX[i];
|
|
}
|
|
|
|
return (float(sum) / float(MS_IN_HOUR)) * 100;
|
|
}
|
|
|
|
// Minutes we must be silent before sending again. Does not sync, and walks the ring as if the index
|
|
// were an age; both are wrong and both are pinned by characterisation tests. See airtime.h's TODO.
|
|
uint8_t AirTime::Windows::getSilentMinutes(float txPercent, float dutyCycle, const Held &)
|
|
{
|
|
float newTxPercent = txPercent;
|
|
for (int8_t i = MINUTES_IN_HOUR - 1; i >= 0; --i) {
|
|
newTxPercent -= ((float)this->utilizationTX[i] / (MS_IN_MINUTE * MINUTES_IN_HOUR / 100));
|
|
if (newTxPercent < dutyCycle)
|
|
return MINUTES_IN_HOUR - 1 - i;
|
|
}
|
|
|
|
return MINUTES_IN_HOUR;
|
|
}
|
|
|
|
// --- the locking shell --------------------------------------------------------------------------
|
|
// Each takes the lock exactly once and delegates. Nothing below calls another method on `this`.
|
|
|
|
void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms)
|
|
{
|
|
{
|
|
Held held(this);
|
|
w.logAirtime(reportType, airtime_ms, held);
|
|
}
|
|
|
|
// Outside the lock: DEBUG_PORT.log() blocks on a UART write, and `lock` is a plain binary
|
|
// semaphore with no priority inheritance, so holding it here would stall the radio thread.
|
|
if (reportType == TX_LOG) {
|
|
LOG_DEBUG("Packet TX: %ums", airtime_ms);
|
|
} else if (reportType == RX_LOG) {
|
|
LOG_DEBUG("Packet RX: %ums", airtime_ms);
|
|
} else if (reportType == RX_ALL_LOG) {
|
|
LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms);
|
|
}
|
|
}
|
|
|
|
void AirTime::airtimeRotatePeriod()
|
|
{
|
|
// Preserve the public helper while keeping all rotation logic in one monotonic-time path.
|
|
Held held(this);
|
|
w.syncNow(held);
|
|
}
|
|
|
|
bool AirTime::airtimeReport(reportTypes reportType, uint32_t *out, size_t count)
|
|
{
|
|
Held held(this);
|
|
return w.airtimeReport(reportType, out, count, held);
|
|
}
|
|
|
|
uint32_t AirTime::getSecondsSinceBoot()
|
|
{
|
|
// Keep HTTP/debug reporting aligned with the same monotonic clock used by the buckets.
|
|
Held held(this);
|
|
w.syncNow(held);
|
|
return w.secSinceBoot;
|
|
}
|
|
|
|
float AirTime::channelUtilizationPercent()
|
|
{
|
|
Held held(this);
|
|
return w.channelUtilizationPercent(held);
|
|
}
|
|
|
|
float AirTime::smoothedChannelUtilizationPercent()
|
|
{
|
|
Held held(this);
|
|
return w.smoothedChannelUtilizationPercent(held);
|
|
}
|
|
|
|
float AirTime::utilizationTXPercent()
|
|
{
|
|
Held held(this);
|
|
return w.utilizationTXPercent(held);
|
|
}
|
|
|
|
// These lock like everything else, because they call the core rather than the public accessors.
|
|
// Both read under the lock and warn after it, for the reason logAirtime() does.
|
|
bool AirTime::isTxAllowedChannelUtil(bool polite)
|
|
{
|
|
uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent);
|
|
float utilization;
|
|
{
|
|
Held held(this);
|
|
utilization = w.channelUtilizationPercent(held);
|
|
}
|
|
|
|
if (utilization < percentage)
|
|
return true;
|
|
LOG_WARN("Ch. util >%d%%. Skip send", percentage);
|
|
return false;
|
|
}
|
|
|
|
bool AirTime::isTxAllowedAirUtil()
|
|
{
|
|
float effectiveDutyCycle = getEffectiveDutyCycle();
|
|
if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) {
|
|
float limit = effectiveDutyCycle * polite_duty_cycle_percent / 100;
|
|
float utilization;
|
|
{
|
|
Held held(this);
|
|
utilization = w.utilizationTXPercent(held);
|
|
}
|
|
|
|
if (utilization < limit)
|
|
return true;
|
|
LOG_WARN("TX air util. >%f%%. Skip send", limit);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint8_t AirTime::getSilentMinutes(float txPercent, float dutyCycle)
|
|
{
|
|
Held held(this);
|
|
return w.getSilentMinutes(txPercent, dutyCycle, held);
|
|
}
|
|
|
|
AirTime::AirTime() : concurrency::OSThread("AirTime") {}
|
|
|
|
int32_t AirTime::runOnce()
|
|
{
|
|
uint32_t rotations;
|
|
{
|
|
Held held(this);
|
|
w.syncNow(held);
|
|
rotations = w.rotationsPendingLog;
|
|
w.rotationsPendingLog = 0;
|
|
}
|
|
|
|
// Outside the lock, for the reason logAirtime() gives. Any caller can cross an hour, but only
|
|
// this thread reports it, so a crossing raised elsewhere is traced at most one tick late.
|
|
if (rotations > 0) {
|
|
LOG_DEBUG("Rotate airtimes, crossed %u hour(s)", rotations);
|
|
}
|
|
|
|
return (1000 * 1);
|
|
}
|