mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-10 15:43:36 -04:00
* time: add skipZero/safeMillis/timerEndsAtMillis helpers
skipZero() steps a millis value past 0, since stored stamps and deadlines
conventionally use 0 for "unset" and the one tick per ~49.7-day wrap that
lands on 0 would otherwise read as never-set.
safeMillis() covers a bare stamp; timerEndsAtMillis(delayMs) covers a
deadline, where the sum is what has to dodge 0 - a non-zero read plus a
delay lands there once per wrap - so it is not safeMillis() + delayMs.
* time: replace hand-rolled zero-dodging with the UptimeClock helpers
PacketHistory rxTimeMsec, EncryptedStorage s_lastFailMillis (stamps), and
SGM41562 lastRefreshMs_ / NextHopRouter learnedAtMsec (ternary stamps) each
hand-rolled skipZero() in place; swap in safeMillis()/skipZero() directly.
HapticFeedback pulseOffAt/delayedPulseAt and GPS fixHoldEnds hand-rolled the
deadline form - millis() + delay, then remap a 0 result to 1 - swap in
timerEndsAtMillis(delay).
No behavior change; each site keeps the value it already computed.
* time: guard the remaining 0-means-unset deadline/stamp writes
rebootAtMsec, shutdownAtMsec, and NotificationRenderer::alertBannerUntil are
all read back with a bare == 0 / != 0 check for 'not scheduled', but every
write site computed millis() + delay (or a bare millis() stamp) with no
guard against landing exactly on 0 - the same wrap hazard skipZero() exists
for, just never applied here.
Route every rebootAtMsec/shutdownAtMsec/alertBannerUntil write through
timerEndsAtMillis()/safeMillis(); RadioLibInterface's reboot-on-stuck-tx
sums an already-captured stamp rather than "now", so it goes through
skipZero() directly instead.
No behavior change outside the ~1-in-2^32 wrap window each site was
already exposed to.
* time: guard three more 0-means-unset deadline writes
ntp_renew (ethClient.cpp), suppressTouchTapUntilMs (Events.cpp), and tx_after
(RadioLibInterface.cpp) all read back 0 as a real state - forced NTP renewal,
no suppress window active, no TX delay armed, respectively - but each arm
site wrote a bare millis()/getMillis() + delay with no guard against the sum
landing exactly on 0.
Route each through Time::timerEndsAtMillis(). No behavior change outside the
wrap window each site was already exposed to.
Refresh the Throttle.h TODO list to note ntp_renew is converted too.
* motion: guard the calibration deadline and use Throttle::deadlinePassed
endCalibrationAt's arm site wrote millis() + calibrateFor with no guard
against landing on 0, the same value finishCalibrationIfExpired()/
drawFrameCalibration() treat as "not calibrating". Route it through
Time::timerEndsAtMillis().
Also swap finishCalibrationIfExpired()'s hand-rolled (int32_t)(now - deadline)
< 0 for Throttle::deadlinePassed(): same wrap-safe comparison the codebase
already provides, without the signed-cast pattern Throttle.h documents as
implementation-defined past INT32_MAX, and it drops the file's last direct
millis() call in favor of the Time:: wrapper the rest of it already uses.
* time: fix Throttle::execute()'s own zero-dodging
Both places execute() writes *lastExecutionMs - the first-ever-run branch
and the regular update - used bare Time::getMillis() with no guard against
landing on 0, which is the exact sentinel this function reads back as
"never run" one line above. A hit there makes the next call re-fire
immediately instead of respecting minumumIntervalMs.
Capture now via Time::safeMillis() once; every use downstream (the elapsed
comparison, the stored value) is then safe by construction instead of
needing the guard reapplied at each write.
* revert some safeMillis cases where overflow is a bad thing
* test(uptime): pin skipZero/safeMillis/timerEndsAtMillis at the wrap boundary
Covers the zero case, an ordinary nonzero value, and a sum that lands
exactly on 0 from a nonzero start - the case timerEndsAtMillis() exists
for, and the one the prior suite had no direct coverage of.
* time: restore the route-health write normalization and put it on one clock
noteRouteLearned()/noteRouteSuccess() lost their `now ? now : 1` normalization,
leaving learnedAtMsec able to store 0 - which getOrAllocRouteHealth() reads as an
ever-growing age, making the slot the first eviction candidate and permanently
stale. Normalize at the write, where the block comment already says it happens,
so every caller is covered rather than just today's two.
Both callers, the two isRouteStale() sites and doRetransmissions() now read
Time::getMillis(), so the stamp and every comparison against it share a clock.
doRetransmissions() goes back to getMillis(): its `now` feeds only comparisons,
never a 0-sentinel field, so skipping zero there only cost accuracy.
* time: read the haptic, InkHUD and calibration deadlines on the write's clock
These three deadlines were converted to Time::timerEndsAtMillis() on the write
side while their reads stayed on millis(), so each spanned two clocks and would
fire immediately or never under an injected test clock. Convert the reads to
match: HapticFeedback::scheduleNext()/runOnce(), the InkHUD tap-suppression
window, and the calibration countdown's read-back of screen->getEndCalibration().
MotionSensor's sampledAtMs is left alone - its write and read are both millis()
and consistent already.
* time: correct the sentinel notes to match what the code actually does
The Throttle.h enumeration claimed the remaining timerEndsAtMillis() callers
"already dodge the sentinel", which reads as a completeness claim the same branch
contradicts: RadioLibInterface's tx_after and activeReceiveStart are both 0=unarmed
and both still arm from bare millis(). Name them instead, so the deadline-type
conversion has the real list. The ntp_renew entry now separates a deliberate 0
("due now", forced at link-up) from a computed one, which is what changed there.
The three TODO(elapsed-stamp) blocks ran four and five lines against the repo's
one-or-two rule, and two of them argued their case wrongly. Throttle.cpp implied
safeMillis() simply doesn't help; in fact neither store is safe on the wrap tick -
the 1 underflows a same-instant read, the 0 re-takes the never-run branch - which
is the symmetry worth recording. PacketHistory.cpp called its dodge "reflecting
the previous pattern" when it is load-bearing: rxTimeMsec 0 means "empty slot"
(PacketHistory.h:21) and insert() drops a record stamped 0 outright, so without it
a packet arriving on the wrap tick is never stored and loses its dedup.
Also picks up trunk fmt's trailing-whitespace fix in Throttle.cpp and the comment
realignment in SGM41562.cpp that this branch's added comment knocked out.
* test(nexthop): pin the route-health stamp against the 0 sentinel
The uptime suite covers skipZero/safeMillis/timerEndsAtMillis themselves, but
nothing covered a call site, so the branch deleted noteRouteLearned()'s
normalization and stayed green. None of the existing route-health tests pass 0 as
`now` - they use 1000, learnAt, or millis() - (TTL + 5000) - which is exactly the
gap the regression went through.
Both new tests fail with "Expected 0 to be not equal to 0" when the skipZero() is
backed out of NextHopRouter, and pass with it. noteRouteSuccess() only refreshes
an existing record, so its twin learns a route first to reach the write.
Also drops a self-referential assertion in the uptime suite: comparing
getMillis() against safeMillis() passes even if safeMillis() does no dodge at
all, so it now asserts the literal.
* discard safemillis for skipzero (better semantics and therefore maintainability) and make consistent use of getmillis where it is called (to permit testing)
* more wrapzero safety
* STM gets some too
* time: stop the next 0-means-unset deadline being armed from raw millis()
The fields this branch armed through Time::timerEndsAtMillis() / Time::skipZero()
are the kind that get added by copy-paste: `rebootAtMsec = millis() + N` appears
at twenty-odd sites across six files, and the next module to defer a reboot will
be written from one of them. Nothing catches the mistake afterwards - the sum
lands on 0 for one tick per ~49.7-day wrap, so a test run, a soak and a bench
session all pass while a pending reboot, shutdown, DFU jump or banner expiry is
silently dropped.
Two guards, at the two places it can go wrong.
The helpers themselves: skipZero() is constexpr, so its contract is now pinned by
static_assert in the header rather than only by test_uptime_clock. The asserts are
chosen against the two plausible rewrites - `ms | 1` perturbs every even value and
`ms + 1` turns the last tick of the wrap into the 0 the function exists to avoid.
Both compile, and both pass a test that only checks skipZero(0); each trips a
distinct assert here, naming the failure mode.
The call sites: bin/lint-unset-sentinel-millis.sh flags a sentinel field in src/
assigned from a raw millis()/getMillis() read, and names the helper to use. It is
name-driven because the 0 contract is declared in src/main.h and enforced in six
other files, so no single-file scan can infer it; every one of the thirteen fields
was checked to actually test against 0 before being listed. nagCycleCutoff and
LinuxJoystick's nextRepeatX/nextRepeatY are deliberately absent - their unset state
is a separate bool - and the nine remaining `millis() + x` sites in src/ are locals
that never store 0 for anything to misread.
Blocking, unlike its note-level neighbours: there is no run-time enforcer to pair
with, and the tree has zero violations today, so gating costs nothing. Scoped to
src/ so test_uptime_clock can keep building raw wrap values on purpose.
bin/test-lint-unset-sentinel-millis.sh pins the scanner against 23 fixtures -
reads, disarms, shadowing locals, comments, string literals and the already-fixed
forms all have to stay quiet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* time: guard the four 0-means-unset stamps this branch had missed
Sweeping src/ for the `if (stamp && <deadline check>)` idiom - the shape that
makes 0 mean "unset" - turned up four stamps still armed from a raw clock read,
so the new lint rule would have had to either ignore them or go red on checkout.
Each is the same one-tick-per-wrap hole the rest of the branch closes:
* TrackballInterruptBase lastInterruptTime, armed in all four ISR handlers and
explicitly disarmed to 0 at the threshold reset. getMillis() is the ISR-safe
read by construction - it compiles to millis() outside PIO_UNIT_TESTING - and
skipZero() is pure, so neither adds anything to interrupt context.
* NeighborInfoModule lastSentReply, read as `if (lastSentReply && ...)` before
the 3-minute reply throttle. Needed the UptimeClock.h include.
* PositionModule lastSentReply, same throttle; already on the injectable clock
but still missing the guard.
* NodeDB lastSort, whose own read spells the sentinel out as `lastSort == 0 ||`.
On the wrap tick each would read as never-stamped: a trackball debounce window
lost, a neighbour or position reply sent inside the throttle it was meant to
respect, one extra NodeDB sort. Cheap individually, which is why they were missed.
All four are now listed in bin/lint-unset-sentinel-millis.sh, so the rule covers
every field in the tree that actually tests against 0 rather than a subset, and
the header records the eight stamps left off for the opposite reason - their unset
state is a separate flag (isNagging, busyTx, heldX/heldY, formatted_this_boot,
heartbeat, gotwind, haveSample, lastIaqValid), so 0 is a value they may legally
hold. The rule is silent across src/ on this tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* lint: let a site opt out of the sentinel rule, with its reason on the record
The rule is blocking, so it needs an escape hatch for the site where 0 genuinely
is a legal timestamp - and the hatch should cost something, or it becomes the
first thing anyone reaches for. `unset-sentinel-ok: <reason>` in a comment on the
write, or on a comment line above it, suppresses that one statement:
// unset-sentinel-ok: busyTx carries the armed state, so 0 is a legal stamp here
lastTxStart = Time::getMillis();
The reason is mandatory. A bare `unset-sentinel-ok`, or a colon with nothing
after it, is reported instead of honoured - with a message saying so - so the
only way to silence a site is to write down why it is safe. trunk-ignore still
works, but this states the justification at the write and also applies when the
script runs outside trunk.
The marker is read from comment text collected during the same character-level
pass that strips comments and literals, not by re-scanning the raw line. That is
what keeps it out of reach of data: LOG_DEBUG("unset-sentinel-ok: ...") mutes
nothing, because a string literal is not a comment. It is also consumed by the
statement it was written for, so it cannot leak onto the next write - while still
carrying across any number of intervening comment lines to the statement below,
which is where a real justification wants to be written.
Twelve fixtures added for the new behaviour: both comment styles, block and
multi-line block comments, the bare form, the marker-in-a-string cases, and three
leak cases. 35 total, all green, under bash 3.2 as well.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* lint: watch the separate-flag stamps too, with their exemption stated at the write
The nine stamps whose armed state lives in a companion boolean were previously
just absent from the rule's list, which meant the reasoning for leaving them out
existed only as prose in a shell script. They are now listed and individually
opted out at the write, naming the flag that actually carries the armed state:
// unset-sentinel-ok: haveSample carries the armed state, so 0 is a legal stamp
lastSampleMs = Time::getMillis();
The point is what happens later. If someone rewrites `if (haveSample && ...)` as
`if (lastSampleMs && ...)`, the field has silently acquired the 0 contract; with
the opt-out sitting at the write, the claim to re-examine is in front of whoever
makes that edit instead of buried in bin/.
Every exemption was checked against its real read sites before being written, and
three candidates did not survive that check. They stay off the list, because
listing one would mean stamping an opt-out over a claim that does not hold:
* nagCycleCutoff. handleInputEvent reads `if (nagCycleCutoff != UINT32_MAX)`
without consulting isNagging, so at that read the field is its own armed flag
with UINT32_MAX as the sentinel - and the arm at ExternalNotificationModule
.cpp:521 can land exactly there. skipZero() cannot help: it lifts 0 to 1 and
leaves UINT32_MAX alone, which UptimeClock.h's own static_assert pins. There
is also a live boot-state bug behind this - the in-class initializer is 1
while isNagging starts false - and fixing the read is a behaviour change that
belongs in its own PR.
* TouchScreenBase::_start. Overloaded as an event stamp AND a `+ 30000`
suppression deadline compared by signed subtraction, so a near-zero value
reads as "long ago" rather than "armed 30s out" and LONG_PRESS re-fires.
skipZero() does not fix this one either: 1 reads as long-ago exactly as 0
does. It needs the stamp and the deadline held separately.
* StoreForwardModule::retry_delay. No reads at all today, so nothing misbehaves
yet; exempting it now would pre-approve the raw arm for whoever implements the
retry its own comment promises.
The rule is silent across src/ on this tree, and the header records all three
rejections so the next person does not have to re-derive them. The self-test's
negative fixture no longer uses nagCycleCutoff as its example of a safely
unlisted field - that would have encoded the opposite of what the header says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(time,lint): guard the recomputed tx_after, and judge one write at a time
Two review findings, both real.
setTransmitDelay() recomputes p->tx_after from a clamp of three candidates, and
that recomputation was still raw. Two lines above it, `if (p->tx_after)` is the read
that takes 0 as "no delay wanted", so a clamp landing on 0 drops the CSMA backoff
and the packet goes out immediately instead of after its computed delay. The first
arm site in this function was already guarded; this one was missed because the
value is not a plain `now + delay` and so does not fit timerEndsAtMillis() - it
takes skipZero() instead.
The narrowing order matters here and is spelled out at the site: add_delay is
unsigned long, 64-bit on the portduino host, so the clamp can exceed UINT32_MAX
there. skipZero() on the wide value would pass 0x100000000 through as non-zero and
the store to this uint32_t field would then truncate it back to the 0 being
avoided, so the cast comes first.
The lint rule judged each write by the wrong text. rhs was taken from the write to
the end of the accumulated statement, so a neighbour on the same line decided the
verdict - and it was wrong in both directions:
rebootAtMsec = millis() + 5; shutdownAtMsec = Time::timerEndsAtMillis(10);
the later helper call suppressed a genuine raw arm
rebootAtMsec = otherDeadline; shutdownAtMsec = millis();
the later millis() reported a safe copy
rhs is now cut at its own semicolon. Six fixtures cover it, including both cases
above, two raw writes on one line, two helper writes on one line, and a statement
split across lines, which must still see its whole right-hand side. 41 fixtures
total, green under bash 3.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
608 lines
25 KiB
C++
608 lines
25 KiB
C++
#include "PacketHistory.h"
|
|
#include "configuration.h"
|
|
#include "memory/MemAudit.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "meshUtils.h"
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "platform/portduino/PortduinoGlue.h"
|
|
#endif
|
|
#include "Throttle.h"
|
|
#include "UptimeClock.h"
|
|
|
|
#define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min
|
|
|
|
#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging
|
|
#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots
|
|
|
|
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initialize members
|
|
{
|
|
if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense
|
|
LOG_WARN("Packet History - Invalid size %u, using default %u", static_cast<unsigned>(size),
|
|
static_cast<unsigned>(PACKETHISTORY_MAX));
|
|
size = PACKETHISTORY_MAX; // Use default size if invalid
|
|
}
|
|
|
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
|
// Ensure capacity fits in uint16_t hash index (HASH_EMPTY = 0xFFFF is the sentinel)
|
|
if (size >= HASH_EMPTY) {
|
|
LOG_WARN("Packet History - Clamping size %u to %u (hash index limit)", static_cast<unsigned>(size),
|
|
static_cast<unsigned>(HASH_EMPTY - 1));
|
|
size = HASH_EMPTY - 1;
|
|
}
|
|
#endif
|
|
|
|
// Allocate memory for the recent packets array
|
|
recentPacketsCapacity = size;
|
|
recentPackets.reset(new PacketRecord[recentPacketsCapacity]);
|
|
if (!recentPackets) { // No logging here, console/log probably uninitialized yet.
|
|
LOG_ERROR("Packet History - Memory allocation failed for size=%u entries / %zu Bytes", static_cast<unsigned>(size),
|
|
sizeof(PacketRecord) * recentPacketsCapacity);
|
|
recentPacketsCapacity = 0; // mark allocation fail
|
|
return; // return early
|
|
}
|
|
|
|
// Initialize the recent packets array to zero
|
|
memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
|
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity);
|
|
|
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
|
// Allocate hash index with load factor <= 0.5 for short probe chains
|
|
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
|
|
hashMask = hashCapacity - 1;
|
|
hashIndex.reset(new uint16_t[hashCapacity]);
|
|
if (!hashIndex) {
|
|
LOG_ERROR("Packet History - Hash index allocation failed for %u entries", static_cast<unsigned>(hashCapacity));
|
|
hashCapacity = 0;
|
|
hashMask = 0;
|
|
return;
|
|
}
|
|
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
|
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity + sizeof(uint16_t) * hashCapacity);
|
|
#endif
|
|
}
|
|
|
|
/** Update recentPackets and return true if we have already seen this packet */
|
|
bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop,
|
|
bool *wasUpgraded)
|
|
{
|
|
if (!initOk()) {
|
|
LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED");
|
|
return false;
|
|
}
|
|
|
|
if (p->id == 0) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: ID 0, not floodable");
|
|
#endif
|
|
return false; // Not a floodable message ID, so we don't care
|
|
}
|
|
|
|
PacketRecord r;
|
|
memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero
|
|
|
|
// Save basic info from checked packet
|
|
r.id = p->id;
|
|
r.sender = getFrom(p); // If 0 then use our ID
|
|
r.next_hop = p->next_hop;
|
|
setHighestHopLimit(r, p->hop_limit);
|
|
bool weWillRelay = false;
|
|
uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum());
|
|
if (p->relay_node == ourRelayID) { // If the relay_node is us, store it
|
|
weWillRelay = true;
|
|
setOurTxHopLimit(r, p->hop_limit);
|
|
r.relayed_by[0] = p->relay_node;
|
|
}
|
|
|
|
// TODO(elapsed-stamp): 0 means "empty slot" here and insert() drops a record stamped 0, so the
|
|
// dodge is important; a same-instant `now - rxTimeMsec` read still underflows to a huge age.
|
|
r.rxTimeMsec = Time::skipZero(Time::getMillis());
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG(
|
|
"Packet History - Was Seen Recently: @start s=0x%08x id=0x%08x / to=0x%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d",
|
|
r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1,
|
|
weWereNextHop ? *weWereNextHop : -1);
|
|
#endif
|
|
|
|
PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array
|
|
bool seenRecently = (found != NULL); // If found -> the packet was seen recently
|
|
|
|
// Check for hop_limit upgrade scenario
|
|
if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) {
|
|
LOG_TRACE("Packet History - Hop limit upgrade: packet 0x%08x hop_limit=%d -> %d", p->id, getHighestHopLimit(*found),
|
|
p->hop_limit);
|
|
*wasUpgraded = true;
|
|
} else if (wasUpgraded) {
|
|
*wasUpgraded = false; // Initialize to false if not an upgrade
|
|
}
|
|
|
|
if (seenRecently) {
|
|
if (wasFallback) {
|
|
// If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already
|
|
// before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle
|
|
// it now.
|
|
if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE &&
|
|
found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) &&
|
|
!wasRelayer(ourRelayID, *found) &&
|
|
!wasRelayer(
|
|
found->next_hop,
|
|
*found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: f=0x%08x id=0x%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE",
|
|
p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1);
|
|
#endif
|
|
*wasFallback = true;
|
|
} else {
|
|
// debug log only
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: f=0x%08x id=0x%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change",
|
|
p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Check if we were the next hop for this packet
|
|
if (weWereNextHop) {
|
|
*weWereNextHop = (found->next_hop == ourRelayID);
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: f=0x%08x id=0x%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s",
|
|
p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if (withUpdate) {
|
|
if (found != NULL) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: s=0x%08x id=0x%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE",
|
|
found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2],
|
|
millis() - found->rxTimeMsec);
|
|
#endif
|
|
// Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1)
|
|
uint8_t startIdx = weWillRelay ? 1 : 0;
|
|
if (!weWillRelay) {
|
|
bool weWereRelayer = wasRelayer(ourRelayID, *found);
|
|
// We were a relayer and the packet came in with a hop limit that is one less than when we sent it out
|
|
if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) {
|
|
r.relayed_by[0] = p->relay_node;
|
|
startIdx = 1; // Start copying existing relayers from index 1
|
|
}
|
|
// keep the original ourTxHopLimit
|
|
setOurTxHopLimit(r, getOurTxHopLimit(*found));
|
|
}
|
|
|
|
// Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has
|
|
// fewer hops remaining.
|
|
if (getHighestHopLimit(*found) > getHighestHopLimit(r))
|
|
setHighestHopLimit(r, getHighestHopLimit(*found));
|
|
|
|
// Add the existing relayed_by to the new record, avoiding duplicates
|
|
for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) {
|
|
if (found->relayed_by[i] == 0)
|
|
continue;
|
|
|
|
bool exists = false;
|
|
for (uint8_t j = 0; j < NUM_RELAYERS; j++) {
|
|
if (r.relayed_by[j] == found->relayed_by[i]) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!exists) {
|
|
r.relayed_by[i + startIdx] = found->relayed_by[i];
|
|
}
|
|
}
|
|
r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked)
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: s=0x%08x id=0x%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER",
|
|
r.sender, r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec);
|
|
#endif
|
|
// TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this
|
|
}
|
|
insert(r); // Insert or update the packet record in the history
|
|
}
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - Was Seen Recently: @exit s=0x%08x id=0x%08x (to=0x%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d "
|
|
"found?%s seenRecently?%s wUpd?%s",
|
|
r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec,
|
|
found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO ");
|
|
#endif
|
|
|
|
return seenRecently;
|
|
}
|
|
|
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
|
// Hash function for (sender, id) pairs. Uses xor-shift mixing for good distribution.
|
|
uint32_t PacketHistory::hashSlot(NodeNum sender, PacketId id) const
|
|
{
|
|
uint32_t h = sender ^ (id * 0x9E3779B9); // Fibonacci hashing constant
|
|
h ^= h >> 16;
|
|
h *= 0x45d9f3b;
|
|
h ^= h >> 16;
|
|
return h & hashMask;
|
|
}
|
|
|
|
void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx)
|
|
{
|
|
if (!hashIndex)
|
|
return;
|
|
uint32_t bucket = hashSlot(sender, id);
|
|
// Guard against infinite loop if hash table is corrupted (no HASH_EMPTY slots)
|
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
|
if (hashIndex[bucket] == HASH_EMPTY) {
|
|
hashIndex[bucket] = slotIdx;
|
|
return;
|
|
}
|
|
bucket = (bucket + 1) & hashMask;
|
|
}
|
|
LOG_ERROR("Packet History - hashInsert: table full or corrupt, rebuild");
|
|
hashRebuild();
|
|
}
|
|
|
|
void PacketHistory::hashRemove(NodeNum sender, PacketId id)
|
|
{
|
|
if (!hashIndex)
|
|
return;
|
|
uint32_t bucket = hashSlot(sender, id);
|
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
|
if (hashIndex[bucket] == HASH_EMPTY)
|
|
return;
|
|
uint16_t idx = hashIndex[bucket];
|
|
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
|
|
// Found it - delete and re-insert subsequent entries to maintain probe chain integrity
|
|
hashIndex[bucket] = HASH_EMPTY;
|
|
uint32_t next = (bucket + 1) & hashMask;
|
|
for (uint32_t j = 0; j < hashCapacity; j++) {
|
|
if (hashIndex[next] == HASH_EMPTY)
|
|
break;
|
|
uint16_t displaced = hashIndex[next];
|
|
hashIndex[next] = HASH_EMPTY;
|
|
if (displaced < recentPacketsCapacity) {
|
|
const auto &rec = recentPackets[displaced];
|
|
hashInsert(rec.sender, rec.id, displaced);
|
|
}
|
|
next = (next + 1) & hashMask;
|
|
}
|
|
return;
|
|
}
|
|
bucket = (bucket + 1) & hashMask;
|
|
}
|
|
}
|
|
|
|
void PacketHistory::hashRebuild()
|
|
{
|
|
if (!hashIndex)
|
|
return;
|
|
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity);
|
|
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
|
|
if (recentPackets[i].rxTimeMsec != 0)
|
|
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/** Find a packet record in history using the hash index for O(1) average lookup.
|
|
* Falls back to linear scan if hash index is unavailable.
|
|
* @return pointer to PacketRecord if found, NULL if not found */
|
|
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
|
{
|
|
if (sender == 0 || id == 0) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - find: s=0x%08x id=0x%08x sender/id=0->NOT FOUND", sender, id);
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
|
// Use hash index for O(1) lookup when available
|
|
if (hashIndex) {
|
|
uint32_t bucket = hashSlot(sender, id);
|
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
|
if (hashIndex[bucket] == HASH_EMPTY)
|
|
break;
|
|
uint16_t idx = hashIndex[bucket];
|
|
if (idx < recentPacketsCapacity && recentPackets[idx].id == id && recentPackets[idx].sender == sender) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - find: s=0x%08x id=0x%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d",
|
|
recentPackets[idx].sender, recentPackets[idx].id, recentPackets[idx].next_hop,
|
|
recentPackets[idx].relayed_by[0], recentPackets[idx].relayed_by[1], recentPackets[idx].relayed_by[2],
|
|
millis() - (recentPackets[idx].rxTimeMsec), idx, recentPacketsCapacity);
|
|
#endif
|
|
return &recentPackets[idx];
|
|
}
|
|
bucket = (bucket + 1) & hashMask;
|
|
}
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - find: s=0x%08x id=0x%08x NOT FOUND", sender, id);
|
|
#endif
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
|
|
PacketRecord *base = recentPackets.get();
|
|
for (PacketRecord *it = base; it < (base + recentPacketsCapacity); ++it) {
|
|
if (it->id == id && it->sender == sender) {
|
|
return it;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/** Insert/Replace oldest PacketRecord in recentPackets. */
|
|
void PacketHistory::insert(const PacketRecord &r)
|
|
{
|
|
uint32_t now_millis = millis(); // Should not jump with time changes
|
|
uint32_t OldtrxTimeMsec = 0;
|
|
PacketRecord *base = recentPackets.get();
|
|
PacketRecord *tu = NULL; // Will insert here.
|
|
PacketRecord *it = NULL;
|
|
|
|
// Find a free, matching or oldest used slot in the recentPackets array
|
|
for (it = base; it < (base + recentPacketsCapacity); ++it) {
|
|
if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty
|
|
tu = it; // Remember the free slot
|
|
#if VERBOSE_PACKET_HISTORY >= 2
|
|
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - base, recentPacketsCapacity);
|
|
#endif
|
|
// We have that, Exit the loop
|
|
it = (base + recentPacketsCapacity);
|
|
} else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert
|
|
tu = it; // Remember the matching slot
|
|
OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age
|
|
#if VERBOSE_PACKET_HISTORY >= 2
|
|
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
|
#endif
|
|
// We have that, Exit the loop
|
|
it = (base + recentPacketsCapacity);
|
|
} else {
|
|
if (it->rxTimeMsec == 0) {
|
|
LOG_WARN("Packet History - insert: Found s=0x%08x id=0x%08x rxTimeMsec = 0, slot %d/%d. Should never happen",
|
|
it->sender, it->id, it - base, recentPacketsCapacity);
|
|
}
|
|
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
|
|
OldtrxTimeMsec = now_millis - it->rxTimeMsec;
|
|
tu = it; // remember the oldest packet
|
|
#if VERBOSE_PACKET_HISTORY >= 2
|
|
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
|
#endif
|
|
}
|
|
// keep looking for oldest till entire array is checked
|
|
}
|
|
}
|
|
|
|
if (tu == NULL) {
|
|
LOG_ERROR("Packet History - insert: No free/matched/oldest slot. Something leaked"); // mx
|
|
// assert(false); // This should never happen, we should always have at least one packet to clear
|
|
return; // Return early if we can't update the history
|
|
}
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
if (tu->id == 0 && tu->sender == 0) {
|
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - base, recentPacketsCapacity);
|
|
} else if (tu->id == r.id && tu->sender == r.sender) {
|
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
|
} else {
|
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
|
}
|
|
#endif
|
|
|
|
// If we are reusing a slot, we should warn if the packet is too recent
|
|
#if RECENT_WARN_AGE > 0
|
|
if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) {
|
|
if (!(tu->id == r.id && tu->sender == r.sender)) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000,
|
|
RECENT_WARN_AGE / 1000);
|
|
#endif
|
|
} else {
|
|
// debug only
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - normal",
|
|
OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if PACKET_HISTORY_TRACE_AGING
|
|
if (tu->rxTimeMsec != 0) {
|
|
LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000.,
|
|
(tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT");
|
|
} else {
|
|
LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.);
|
|
}
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=0x%08x id=0x%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - base,
|
|
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
|
|
tu->rxTimeMsec);
|
|
#endif
|
|
|
|
if (r.rxTimeMsec == 0) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_WARN("Packet History - insert: Won't store packet with rxTimeMsec = 0");
|
|
#endif
|
|
return; // Return early if we can't update the history
|
|
}
|
|
|
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
|
// Maintain hash index: remove old entry if evicting a different packet, then insert new entry
|
|
bool isMatchingSlot = (tu->id == r.id && tu->sender == r.sender);
|
|
if (!isMatchingSlot && tu->rxTimeMsec != 0) {
|
|
hashRemove(tu->sender, tu->id);
|
|
}
|
|
|
|
*tu = r; // store the packet
|
|
|
|
if (!isMatchingSlot) {
|
|
hashInsert(r.sender, r.id, (uint16_t)(tu - base));
|
|
}
|
|
#else
|
|
*tu = r; // store the packet
|
|
#endif
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=0x%08x id=0x%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - base,
|
|
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
|
|
tu->rxTimeMsec);
|
|
#endif
|
|
}
|
|
|
|
/* Check if a certain node was a relayer of a packet in the history given an ID and sender
|
|
* @return true if node was indeed a relayer, false if not */
|
|
bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole)
|
|
{
|
|
if (!initOk()) {
|
|
LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED");
|
|
return false;
|
|
}
|
|
|
|
if (relayer == 0) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - was relayer: s=0x%08x id=0x%08x / rl=%02x=zero. NO", sender, id, relayer);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
const PacketRecord *found = find(sender, id);
|
|
|
|
if (found == NULL) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - was relayer: s=0x%08x id=0x%08x / rl=%02x / PR not found. NO", sender, id, relayer);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
#if VERBOSE_PACKET_HISTORY >= 2
|
|
LOG_DEBUG("Packet History - was relayer: s=0x%08x id=0x%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x",
|
|
found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1],
|
|
found->relayed_by[2], relayer);
|
|
#endif
|
|
return wasRelayer(relayer, *found, wasSole);
|
|
}
|
|
|
|
/* Check if a certain node was a relayer of a packet in the history given iterator
|
|
* @return true if node was indeed a relayer, false if not
|
|
* NOTE: intentionally byte-domain. Both `relayer` and relayed_by[] are on-wire last bytes, so this
|
|
* answers "did a relayer with this byte touch the packet" - correct without resolving to a NodeNum.
|
|
* The collision risk is neutralized where the result is consumed (route learning in
|
|
* NextHopRouter::sniffReceived now gates the write through NodeDB::resolveUniqueLastByte). */
|
|
bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole)
|
|
{
|
|
bool found = false;
|
|
bool other_present = false;
|
|
|
|
for (uint8_t i = 0; i < NUM_RELAYERS; ++i) {
|
|
if (r.relayed_by[i] == relayer) {
|
|
found = true;
|
|
} else if (r.relayed_by[i] != 0) {
|
|
other_present = true;
|
|
}
|
|
}
|
|
|
|
if (wasSole) {
|
|
*wasSole = (found && !other_present);
|
|
}
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - was rel.PR.: s=0x%08x id=0x%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id,
|
|
r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer);
|
|
#endif
|
|
|
|
return found;
|
|
}
|
|
|
|
// Check two relayers against the same packet record with a single find() call,
|
|
// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair.
|
|
void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
|
|
bool *r2WasSole)
|
|
{
|
|
*r1Result = false;
|
|
*r2Result = false;
|
|
if (r2WasSole)
|
|
*r2WasSole = false;
|
|
|
|
if (!initOk()) {
|
|
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED");
|
|
return;
|
|
}
|
|
|
|
const PacketRecord *found = find(sender, id);
|
|
if (!found)
|
|
return;
|
|
|
|
if (relayer1 != 0)
|
|
*r1Result = wasRelayer(relayer1, *found);
|
|
if (relayer2 != 0)
|
|
*r2Result = wasRelayer(relayer2, *found, r2WasSole);
|
|
}
|
|
|
|
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
|
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
|
|
{
|
|
if (!initOk()) {
|
|
LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED");
|
|
return;
|
|
}
|
|
|
|
PacketRecord *found = find(sender, id);
|
|
if (found == NULL) {
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - remove Relayer s=0x%08x id=0x%08x (rl=%02x) NOT FOUND", sender, id, relayer);
|
|
#endif
|
|
return; // Nothing to remove
|
|
}
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - remove Relayer s=0x%08x id=0x%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id,
|
|
found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer);
|
|
#endif
|
|
|
|
// nexthop and rxTimeMsec too stay in found entry
|
|
|
|
uint8_t j = 0;
|
|
uint8_t i = 0;
|
|
for (; i < NUM_RELAYERS; i++) {
|
|
if (found->relayed_by[i] != relayer) {
|
|
found->relayed_by[j] = found->relayed_by[i];
|
|
j++;
|
|
} else
|
|
found->relayed_by[i] = 0;
|
|
}
|
|
for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array
|
|
found->relayed_by[j] = 0;
|
|
}
|
|
|
|
#if VERBOSE_PACKET_HISTORY
|
|
LOG_DEBUG("Packet History - remove Relayer s=0x%08x id=0x%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender,
|
|
found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j);
|
|
#endif
|
|
}
|
|
|
|
// Getters and setters for hop limit fields packed in hop_limit
|
|
inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r)
|
|
{
|
|
return r.hop_limit & HOP_LIMIT_HIGHEST_MASK;
|
|
}
|
|
|
|
inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit)
|
|
{
|
|
r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK);
|
|
}
|
|
|
|
inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r)
|
|
{
|
|
return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT;
|
|
}
|
|
|
|
inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit)
|
|
{
|
|
r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK);
|
|
}
|