-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathairtime.cpp
More file actions
312 lines (265 loc) · 10.4 KB
/
Copy pathairtime.cpp
File metadata and controls
312 lines (265 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#include "airtime.h"
#include "NodeDB.h"
#include "UptimeClock.h"
#include "configuration.h"
#include <assert.h>
#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 &)
{
// 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);
if (elapsedUtilPeriods >= CHANNEL_UTILIZATION_PERIODS) {
memset(this->channelUtilization, 0, sizeof(this->channelUtilization));
} else {
for (uint32_t i = 1; i <= elapsedUtilPeriods; i++) {
this->channelUtilization[((oldSecSinceBoot / 10) + i) % CHANNEL_UTILIZATION_PERIODS] = 0;
}
}
// 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::channelUtilizationPercent(const Held &held)
{
// Gate decisions should see buckets that have decayed across light-sleep time.
syncNow(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::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::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);
}