Add recent repeater prefix search
This commit is contained in:
1 parent
6ddbaf3274
commit
81ff965c7c
10 files changed
+396
-108
No files matched your search
+10
-1
@@ -2655,11 +2655,12 @@ set direct.retry.cr 20.0,12.0,6.0,2.0
|
||||
- `get recent.repeater`
|
||||
- `get recent.repeater <page>`
|
||||
- `get recent.repeaters <page>`
|
||||
- `get recent.repeaters search <prefix> [page]`
|
||||
- `set recent.repeater <prefix> [snr_db]`
|
||||
- `clear recent.repeater`
|
||||
|
||||
**Parameters:**
|
||||
- `prefix`: Repeater path-hash prefix as hex.
|
||||
- `prefix`: Repeater path-hash prefix as 2, 4, or 6 hex characters.
|
||||
- `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`.
|
||||
- `page`: 1-based result page.
|
||||
|
||||
@@ -2668,6 +2669,12 @@ set direct.retry.cr 20.0,12.0,6.0,2.0
|
||||
**Output order:**
|
||||
- `get recent.repeater` lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes.
|
||||
- Within each prefix length, entries are sorted from highest SNR to lowest SNR.
|
||||
- `search` returns every overlapping path-hash entry. For example, searching
|
||||
`860C` can return `86`, `860C`, and `860CCA`; it does not return a different
|
||||
branch such as `86D0`.
|
||||
- Search rows include the monotonic age of the entry's most recent recording,
|
||||
compacted to a whole `s`, `m`, or `h` field. Search pages contain up to six
|
||||
rows so the result remains within the remote CLI reply limit.
|
||||
|
||||
**SNR details:**
|
||||
- Recent repeater SNR is stored internally in quarter-dB units.
|
||||
@@ -2684,6 +2691,8 @@ set direct.retry.cr 20.0,12.0,6.0,2.0
|
||||
```
|
||||
get recent.repeater
|
||||
get recent.repeater 2
|
||||
get recent.repeaters search 86
|
||||
get recent.repeaters search 860C page 2
|
||||
set recent.repeater A1B2C3 8.5
|
||||
set recent.repeater 71CE82 -3.25
|
||||
set recent.repeater A1B2C3
|
||||
|
||||
@@ -79,7 +79,7 @@ set flood.retry.ignore none
|
||||
| `battery.alert` | Sends opt-in, region-scoped low-battery warnings to `#repeaters` after 30 minutes of uptime. | `get battery.alert`, `get battery.alert.region`, `set battery.alert on [region]`, `set battery.alert off` | `set battery.alert on sea` |
|
||||
| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` |
|
||||
| `battery.alert.critical` | Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` |
|
||||
| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Entries older than 24 hours are removed by a three-hour sweep. | `get recent.repeater`, `get recent.repeater <page>`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` |
|
||||
| `recent.repeater` | Shows, searches, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. Search results include the last-recorded age. Entries older than 24 hours are removed by a three-hour sweep. | `get recent.repeater [page]`, `get recent.repeaters search <2|4|6 hex> [page]`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `get recent.repeaters search A1B2` |
|
||||
| `flood.channel.data` | Turns forwarding of flood `GRP_DATA` channel packets on or off. With the default `on`, `GRP_DATA` repeats normally even when `flood.channel.block.hops` is set. | `get flood.channel.data`, `set flood.channel.data on/off` | `set flood.channel.data off` |
|
||||
| `flood.channel.data.hops` | Separate hop gate used only when `flood.channel.data` is `off`; `all` blocks `GRP_DATA` at any hop count, `1`-`7` repeats at that hop count or lower and blocks longer paths. | `get flood.channel.data.hops`, `set flood.channel.data.hops <all|1-7>` | `set flood.channel.data.hops 7` |
|
||||
| `flood.channel.block` | Blocks selected flood `GRP_TXT`/`GRP_DATA` channels when the key validates the packet. New repeater block lists start with editable/deletable `#wardriving h=4`. Add `h=<all|1-7|default>` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] <key|#channel> [name] [h=...]`, `del flood.channel.block[.n]` | `set flood.channel.block #wardriving h=4` |
|
||||
@@ -160,8 +160,14 @@ get recent.repeater
|
||||
get recent.repeater 2
|
||||
get recent.repeaters 2
|
||||
get recent.repeater page 3
|
||||
get recent.repeaters search A1
|
||||
get recent.repeaters search A1B2 page 2
|
||||
```
|
||||
|
||||
Search matches overlapping 1-, 2-, and 3-byte path hashes. Each matching row
|
||||
shows its stored SNR and a whole `s`, `m`, or `h` age measured from the most
|
||||
recent recording. Search pages contain up to six rows.
|
||||
|
||||
Seed or correct a prefix:
|
||||
|
||||
```text
|
||||
@@ -175,10 +181,8 @@ clear recent.repeater
|
||||
```
|
||||
|
||||
Rows are sorted by prefix width, then SNR. A full direct retry failure lowers
|
||||
the matching row by `0.25 dB`.
|
||||
|
||||
Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to
|
||||
`7` rows.
|
||||
the matching row by `0.25 dB`. Unfiltered paged replies contain up to `10`
|
||||
rows; an unpaged local serial query prints the full table.
|
||||
|
||||
## Direct Path Overrides
|
||||
|
||||
|
||||
@@ -2174,70 +2174,75 @@ static void formatLocalSnrX4(char* dest, size_t dest_len, int16_t snr_x4) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseRecentRepeatersPageCommand(const char* command, int& page) {
|
||||
if (strncmp(command, "get ", 4) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* cursor = command + 4;
|
||||
if (strncmp(cursor, "recent.repeater", 15) != 0) {
|
||||
return false;
|
||||
}
|
||||
cursor += 15;
|
||||
|
||||
if (*cursor == 's') {
|
||||
cursor++;
|
||||
}
|
||||
if (*cursor == 0) {
|
||||
return false;
|
||||
}
|
||||
if (*cursor != ' ') {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*cursor == ' ') cursor++;
|
||||
if (strncmp(cursor, "page", 4) == 0 && (cursor[4] == 0 || cursor[4] == ' ')) {
|
||||
cursor += 4;
|
||||
while (*cursor == ' ') cursor++;
|
||||
}
|
||||
|
||||
page = 1;
|
||||
if (*cursor) page = atoi(cursor);
|
||||
if (page < 1) page = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::formatRecentRepeatersReply(char *reply, int page) {
|
||||
void MyMesh::formatRecentRepeatersReply(char *reply, int page,
|
||||
const uint8_t* search_prefix,
|
||||
uint8_t search_prefix_len) {
|
||||
const SimpleMeshTables* tables = static_cast<const SimpleMeshTables*>(getTables());
|
||||
if (tables == NULL) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
return;
|
||||
}
|
||||
int count = tables->getRecentRepeaterCount();
|
||||
const bool is_search = search_prefix != NULL && search_prefix_len > 0;
|
||||
int count = is_search
|
||||
? tables->getRecentRepeaterMatchingCount(search_prefix, search_prefix_len)
|
||||
: tables->getRecentRepeaterCount();
|
||||
if (count <= 0) {
|
||||
strcpy(reply, "> -none-");
|
||||
if (is_search) {
|
||||
char search_hex[MAX_ROUTE_HASH_BYTES * 2 + 1];
|
||||
mesh::Utils::toHex(search_hex, search_prefix, search_prefix_len);
|
||||
search_hex[search_prefix_len * 2] = 0;
|
||||
snprintf(reply, 160, "> %s -none-", search_hex);
|
||||
} else {
|
||||
strcpy(reply, "> -none-");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int page_size = 10;
|
||||
// Search rows include their compact recorded age. Six worst-case rows plus the
|
||||
// page header still fit the 160-byte remote CLI reply buffer.
|
||||
const int page_size = is_search ? 6 : 10;
|
||||
int pages = (count + page_size - 1) / page_size;
|
||||
if (page < 1) page = 1;
|
||||
if (page > pages) page = pages;
|
||||
|
||||
int len = snprintf(reply, 160, "> %d/%d", page, pages);
|
||||
int len;
|
||||
if (is_search) {
|
||||
char search_hex[MAX_ROUTE_HASH_BYTES * 2 + 1];
|
||||
mesh::Utils::toHex(search_hex, search_prefix, search_prefix_len);
|
||||
search_hex[search_prefix_len * 2] = 0;
|
||||
len = snprintf(reply, 160, "> %s %d/%d (%d matches)",
|
||||
search_hex, page, pages, count);
|
||||
} else {
|
||||
len = snprintf(reply, 160, "> %d/%d", page, pages);
|
||||
}
|
||||
int start = (page - 1) * page_size;
|
||||
for (int i = 0; i < page_size && len < 150; i++) {
|
||||
const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterBySortedIdx(start + i);
|
||||
const SimpleMeshTables::RecentRepeaterInfo* info = is_search
|
||||
? tables->getRecentRepeaterMatchingBySortedIdx(
|
||||
search_prefix, search_prefix_len, start + i)
|
||||
: tables->getRecentRepeaterBySortedIdx(start + i);
|
||||
if (info == NULL) break;
|
||||
char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1];
|
||||
char snr[12];
|
||||
mesh::Utils::toHex(prefix, info->prefix, info->prefix_len);
|
||||
prefix[info->prefix_len * 2] = 0;
|
||||
formatLocalSnrX4(snr, sizeof(snr), info->snr_x4);
|
||||
len += snprintf(&reply[len], 160 - len, "\n%s,%s%s",
|
||||
prefix,
|
||||
snr[0] == '-' ? "" : " ",
|
||||
snr);
|
||||
if (is_search) {
|
||||
const uint32_t age_seconds =
|
||||
(uint32_t)(millis() - info->last_heard_millis) / 1000UL;
|
||||
char age[12];
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), age_seconds);
|
||||
len += snprintf(&reply[len], 160 - len, "\n%s,%s%s,%s",
|
||||
prefix,
|
||||
snr[0] == '-' ? "" : " ",
|
||||
snr,
|
||||
age);
|
||||
} else {
|
||||
len += snprintf(&reply[len], 160 - len, "\n%s,%s%s",
|
||||
prefix,
|
||||
snr[0] == '-' ? "" : " ",
|
||||
snr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8153,7 +8158,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
sender == NULL ? NULL : sender->id.pub_key);
|
||||
#endif
|
||||
char* reply_start = reply;
|
||||
int recent_page = 1;
|
||||
|
||||
// Remote admin clients may include a line ending in the command payload.
|
||||
// Normalize it here so exact-match commands such as `get outpath` behave the
|
||||
@@ -8528,8 +8532,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
strcpy(reply, "Err - bad pubkey");
|
||||
}
|
||||
}
|
||||
} else if (sender_timestamp == 0 && sender == NULL && parseRecentRepeatersPageCommand(command, recent_page)) {
|
||||
formatRecentRepeatersReply(reply, recent_page);
|
||||
} else if (sender_timestamp == 0 && sender == NULL
|
||||
&& (strcmp(command, "get recent.repeater") == 0 || strcmp(command, "get recent.repeaters") == 0)) {
|
||||
printRecentRepeatersSerial();
|
||||
|
||||
@@ -782,7 +782,9 @@ public:
|
||||
void formatRadioStatsReply(char *reply) override;
|
||||
void formatRadioDiagReply(char *reply) override;
|
||||
void formatPacketStatsReply(char *reply) override;
|
||||
void formatRecentRepeatersReply(char *reply, int page) override;
|
||||
void formatRecentRepeatersReply(char *reply, int page,
|
||||
const uint8_t* search_prefix,
|
||||
uint8_t search_prefix_len) override;
|
||||
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override;
|
||||
void clearRecentRepeaters() override;
|
||||
void startRegionsLoad() override;
|
||||
|
||||
@@ -23,6 +23,123 @@ enum class NoArgCommandMatch : uint8_t {
|
||||
HasArguments,
|
||||
};
|
||||
|
||||
enum class RecentRepeaterGetMatch : uint8_t {
|
||||
NoMatch = 0,
|
||||
Valid,
|
||||
Invalid,
|
||||
};
|
||||
|
||||
struct RecentRepeaterGetQuery {
|
||||
int page;
|
||||
uint8_t search_prefix[3];
|
||||
uint8_t search_prefix_len;
|
||||
};
|
||||
|
||||
inline const char* skipRecentRepeaterSpaces(const char* text) {
|
||||
while (text != nullptr && (*text == ' ' || *text == '\t')) text++;
|
||||
return text;
|
||||
}
|
||||
|
||||
inline int recentRepeaterHexNibble(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline bool parseRecentRepeaterPage(const char* text, int& page) {
|
||||
text = skipRecentRepeaterSpaces(text);
|
||||
if (text == nullptr || *text == 0) {
|
||||
page = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t parsed = 0;
|
||||
bool saw_digit = false;
|
||||
while (*text >= '0' && *text <= '9') {
|
||||
saw_digit = true;
|
||||
const uint8_t digit = static_cast<uint8_t>(*text++ - '0');
|
||||
// The formatter clamps the requested page to the available page count.
|
||||
// Saturating here avoids signed overflow from hostile remote CLI input.
|
||||
if (parsed <= 65535UL) {
|
||||
parsed = parsed * 10UL + digit;
|
||||
if (parsed > 65535UL) parsed = 65535UL;
|
||||
}
|
||||
}
|
||||
text = skipRecentRepeaterSpaces(text);
|
||||
if (!saw_digit || *text != 0) return false;
|
||||
page = parsed == 0 ? 1 : static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void formatRecentRepeaterAge(char* output, size_t output_size,
|
||||
uint32_t age_seconds) {
|
||||
if (output == nullptr || output_size == 0) return;
|
||||
if (age_seconds >= 3600UL) {
|
||||
snprintf(output, output_size, "%luh",
|
||||
static_cast<unsigned long>(age_seconds / 3600UL));
|
||||
} else if (age_seconds >= 60UL) {
|
||||
snprintf(output, output_size, "%lum",
|
||||
static_cast<unsigned long>(age_seconds / 60UL));
|
||||
} else {
|
||||
snprintf(output, output_size, "%lus",
|
||||
static_cast<unsigned long>(age_seconds));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the portion after `get `. Besides the existing list/page forms, this
|
||||
// accepts `recent.repeaters search <2|4|6 hex> [page [N]|N]`.
|
||||
inline RecentRepeaterGetMatch parseRecentRepeaterGet(
|
||||
const char* config, RecentRepeaterGetQuery& query) {
|
||||
query.page = 1;
|
||||
memset(query.search_prefix, 0, sizeof(query.search_prefix));
|
||||
query.search_prefix_len = 0;
|
||||
if (config == nullptr || strncmp(config, "recent.repeater", 15) != 0) {
|
||||
return RecentRepeaterGetMatch::NoMatch;
|
||||
}
|
||||
|
||||
const char* cursor = config + 15;
|
||||
if (*cursor == 's') cursor++;
|
||||
if (*cursor != 0 && *cursor != ' ' && *cursor != '\t') {
|
||||
return RecentRepeaterGetMatch::NoMatch;
|
||||
}
|
||||
cursor = skipRecentRepeaterSpaces(cursor);
|
||||
if (*cursor == 0) return RecentRepeaterGetMatch::Valid;
|
||||
|
||||
if (strncmp(cursor, "search", 6) == 0
|
||||
&& (cursor[6] == 0 || cursor[6] == ' ' || cursor[6] == '\t')) {
|
||||
cursor = skipRecentRepeaterSpaces(cursor + 6);
|
||||
const char* hex = cursor;
|
||||
while (*cursor != 0 && *cursor != ' ' && *cursor != '\t') cursor++;
|
||||
const size_t hex_len = static_cast<size_t>(cursor - hex);
|
||||
if (hex_len != 2 && hex_len != 4 && hex_len != 6) {
|
||||
return RecentRepeaterGetMatch::Invalid;
|
||||
}
|
||||
for (size_t i = 0; i < hex_len; i += 2) {
|
||||
const int hi = recentRepeaterHexNibble(hex[i]);
|
||||
const int lo = recentRepeaterHexNibble(hex[i + 1]);
|
||||
if (hi < 0 || lo < 0) return RecentRepeaterGetMatch::Invalid;
|
||||
query.search_prefix[i / 2] = static_cast<uint8_t>((hi << 4) | lo);
|
||||
}
|
||||
query.search_prefix_len = static_cast<uint8_t>(hex_len / 2);
|
||||
|
||||
cursor = skipRecentRepeaterSpaces(cursor);
|
||||
if (strncmp(cursor, "page", 4) == 0
|
||||
&& (cursor[4] == 0 || cursor[4] == ' ' || cursor[4] == '\t')) {
|
||||
cursor = skipRecentRepeaterSpaces(cursor + 4);
|
||||
}
|
||||
return parseRecentRepeaterPage(cursor, query.page)
|
||||
? RecentRepeaterGetMatch::Valid : RecentRepeaterGetMatch::Invalid;
|
||||
}
|
||||
|
||||
if (strncmp(cursor, "page", 4) == 0
|
||||
&& (cursor[4] == 0 || cursor[4] == ' ' || cursor[4] == '\t')) {
|
||||
cursor = skipRecentRepeaterSpaces(cursor + 4);
|
||||
}
|
||||
return parseRecentRepeaterPage(cursor, query.page)
|
||||
? RecentRepeaterGetMatch::Valid : RecentRepeaterGetMatch::Invalid;
|
||||
}
|
||||
|
||||
inline StandaloneWiFiKey classifyStandaloneWiFiGet(const char* config) {
|
||||
if (config == nullptr) return StandaloneWiFiKey::None;
|
||||
if (strcmp(config, "wifi.ssid") == 0) return StandaloneWiFiKey::SSID;
|
||||
|
||||
+10
-28
@@ -158,31 +158,6 @@ static bool otaCommandNeedsTempRadio(const char* command) {
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool parseRecentRepeaterGet(const char* config, int& page) {
|
||||
if (strncmp(config, "recent.repeater", 15) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* cursor = &config[15];
|
||||
if (*cursor == 's') {
|
||||
cursor++;
|
||||
}
|
||||
if (*cursor != 0 && *cursor != ' ') {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*cursor == ' ') cursor++;
|
||||
if (strncmp(cursor, "page", 4) == 0 && (cursor[4] == 0 || cursor[4] == ' ')) {
|
||||
cursor += 4;
|
||||
while (*cursor == ' ') cursor++;
|
||||
}
|
||||
|
||||
page = 1;
|
||||
if (*cursor) page = _atoi(cursor);
|
||||
if (page < 1) page = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isValidName(const char *n) {
|
||||
while (*n) {
|
||||
if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
|
||||
@@ -4506,7 +4481,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
strcpy(reply, "Error, retry configuration unsupported on this role");
|
||||
return;
|
||||
}
|
||||
int recent_page = 1;
|
||||
mesh::cli::RecentRepeaterGetQuery recent_query;
|
||||
const mesh::cli::RecentRepeaterGetMatch recent_query_match =
|
||||
mesh::cli::parseRecentRepeaterGet(config, recent_query);
|
||||
if (memcmp(config, "dutycycle", 9) == 0) {
|
||||
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
|
||||
int dc_int = (int)dc;
|
||||
@@ -4677,8 +4654,13 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4);
|
||||
sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8);
|
||||
}
|
||||
} else if (parseRecentRepeaterGet(config, recent_page)) {
|
||||
_callbacks->formatRecentRepeatersReply(reply, recent_page);
|
||||
} else if (recent_query_match == mesh::cli::RecentRepeaterGetMatch::Valid) {
|
||||
_callbacks->formatRecentRepeatersReply(
|
||||
reply, recent_query.page,
|
||||
recent_query.search_prefix_len == 0 ? NULL : recent_query.search_prefix,
|
||||
recent_query.search_prefix_len);
|
||||
} else if (recent_query_match == mesh::cli::RecentRepeaterGetMatch::Invalid) {
|
||||
strcpy(reply, "Error, use: get recent.repeaters [page] | search <2|4|6 hex> [page]");
|
||||
} else if (memcmp(config, "owner.info", 10) == 0) {
|
||||
*reply++ = '>';
|
||||
*reply++ = ' ';
|
||||
|
||||
@@ -279,8 +279,12 @@ public:
|
||||
virtual void formatRadioStatsReply(char *reply) = 0;
|
||||
virtual void formatRadioDiagReply(char *reply) { strcpy(reply, "Not supported"); }
|
||||
virtual void formatPacketStatsReply(char *reply) = 0;
|
||||
virtual void formatRecentRepeatersReply(char *reply, int page) {
|
||||
virtual void formatRecentRepeatersReply(char *reply, int page,
|
||||
const uint8_t* search_prefix = NULL,
|
||||
uint8_t search_prefix_len = 0) {
|
||||
(void)page;
|
||||
(void)search_prefix;
|
||||
(void)search_prefix_len;
|
||||
if (reply != NULL) reply[0] = 0;
|
||||
}
|
||||
virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
|
||||
|
||||
@@ -189,6 +189,50 @@ private:
|
||||
return a_idx < b_idx;
|
||||
}
|
||||
|
||||
bool recentRepeaterMatchesFilter(const RecentRepeaterInfo& info,
|
||||
const uint8_t* search_prefix,
|
||||
uint8_t search_prefix_len) const {
|
||||
if (search_prefix_len == 0) return true;
|
||||
return search_prefix != NULL && search_prefix_len <= MAX_ROUTE_HASH_BYTES
|
||||
&& routeHashPrefixesOverlap(info.prefix, info.prefix_len,
|
||||
search_prefix, search_prefix_len);
|
||||
}
|
||||
|
||||
const RecentRepeaterInfo* getRecentRepeaterBySortedIdxFiltered(
|
||||
int idx_wanted, const uint8_t* search_prefix,
|
||||
uint8_t search_prefix_len) const {
|
||||
if (_max_recent_repeaters == 0 || idx_wanted < 0
|
||||
|| search_prefix_len > MAX_ROUTE_HASH_BYTES
|
||||
|| (search_prefix_len > 0 && search_prefix == NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RecentRepeaterInfo* last = NULL;
|
||||
int last_idx = -1;
|
||||
for (int rank = 0; rank <= idx_wanted; rank++) {
|
||||
const RecentRepeaterInfo* best = NULL;
|
||||
int best_idx = -1;
|
||||
for (int i = 0; i < _recent_repeater_count; i++) {
|
||||
const RecentRepeaterInfo* info = &_recent_repeaters[i];
|
||||
if (!recentRepeaterMatchesFilter(*info, search_prefix,
|
||||
search_prefix_len)) {
|
||||
continue;
|
||||
}
|
||||
if (last != NULL && !recentRepeaterComesBefore(*last, last_idx, *info, i)) {
|
||||
continue;
|
||||
}
|
||||
if (best == NULL || recentRepeaterComesBefore(*info, i, *best, best_idx)) {
|
||||
best = info;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
if (best == NULL) return NULL;
|
||||
last = best;
|
||||
last_idx = best_idx;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
void recordRecentRepeater(const mesh::Packet* packet) {
|
||||
if (_max_recent_repeaters == 0) {
|
||||
return;
|
||||
@@ -419,35 +463,30 @@ public:
|
||||
return _recent_repeater_count;
|
||||
}
|
||||
const RecentRepeaterInfo* getRecentRepeaterBySortedIdx(int idx_wanted) const {
|
||||
if (_max_recent_repeaters == 0) {
|
||||
return NULL;
|
||||
return getRecentRepeaterBySortedIdxFiltered(idx_wanted, NULL, 0);
|
||||
}
|
||||
int getRecentRepeaterMatchingCount(const uint8_t* search_prefix,
|
||||
uint8_t search_prefix_len) const {
|
||||
if (_max_recent_repeaters == 0 || search_prefix == NULL
|
||||
|| search_prefix_len == 0
|
||||
|| search_prefix_len > MAX_ROUTE_HASH_BYTES) {
|
||||
return 0;
|
||||
}
|
||||
if (idx_wanted < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const RecentRepeaterInfo* last = NULL;
|
||||
int last_idx = -1;
|
||||
for (int rank = 0; rank <= idx_wanted; rank++) {
|
||||
const RecentRepeaterInfo* best = NULL;
|
||||
int best_idx = -1;
|
||||
for (int i = 0; i < _recent_repeater_count; i++) {
|
||||
const RecentRepeaterInfo* info = &_recent_repeaters[i];
|
||||
if (last != NULL && !recentRepeaterComesBefore(*last, last_idx, *info, i)) {
|
||||
continue;
|
||||
}
|
||||
if (best == NULL || recentRepeaterComesBefore(*info, i, *best, best_idx)) {
|
||||
best = info;
|
||||
best_idx = i;
|
||||
}
|
||||
int count = 0;
|
||||
for (int i = 0; i < _recent_repeater_count; i++) {
|
||||
if (recentRepeaterMatchesFilter(_recent_repeaters[i], search_prefix,
|
||||
search_prefix_len)) {
|
||||
count++;
|
||||
}
|
||||
if (best == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
last = best;
|
||||
last_idx = best_idx;
|
||||
}
|
||||
return last;
|
||||
return count;
|
||||
}
|
||||
const RecentRepeaterInfo* getRecentRepeaterMatchingBySortedIdx(
|
||||
const uint8_t* search_prefix, uint8_t search_prefix_len,
|
||||
int idx_wanted) const {
|
||||
if (search_prefix == NULL || search_prefix_len == 0) return NULL;
|
||||
return getRecentRepeaterBySortedIdxFiltered(
|
||||
idx_wanted, search_prefix, search_prefix_len);
|
||||
}
|
||||
|
||||
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const {
|
||||
|
||||
@@ -151,6 +151,96 @@ TEST(CLICommandUtils, MatchesDiscoverNeighborsWithoutPrefixCollisions) {
|
||||
"discover.neighbors.extra", "discover.neighbors"));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, ParsesRecentRepeaterListAndPageQueries) {
|
||||
using mesh::cli::RecentRepeaterGetMatch;
|
||||
mesh::cli::RecentRepeaterGetQuery query;
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet("recent.repeater", query));
|
||||
EXPECT_EQ(1, query.page);
|
||||
EXPECT_EQ(0, query.search_prefix_len);
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters page 17", query));
|
||||
EXPECT_EQ(17, query.page);
|
||||
EXPECT_EQ(0, query.search_prefix_len);
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet("recent.repeaters 0", query));
|
||||
EXPECT_EQ(1, query.page);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, ParsesRecentRepeaterHexSearchesAndOptionalPages) {
|
||||
using mesh::cli::RecentRepeaterGetMatch;
|
||||
mesh::cli::RecentRepeaterGetQuery query;
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search 86", query));
|
||||
ASSERT_EQ(1, query.search_prefix_len);
|
||||
EXPECT_EQ(0x86, query.search_prefix[0]);
|
||||
EXPECT_EQ(1, query.page);
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search 860c 2", query));
|
||||
ASSERT_EQ(2, query.search_prefix_len);
|
||||
EXPECT_EQ(0x86, query.search_prefix[0]);
|
||||
EXPECT_EQ(0x0c, query.search_prefix[1]);
|
||||
EXPECT_EQ(2, query.page);
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Valid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeater search 860CCA page 3", query));
|
||||
ASSERT_EQ(3, query.search_prefix_len);
|
||||
EXPECT_EQ(0x86, query.search_prefix[0]);
|
||||
EXPECT_EQ(0x0c, query.search_prefix[1]);
|
||||
EXPECT_EQ(0xca, query.search_prefix[2]);
|
||||
EXPECT_EQ(3, query.page);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, RejectsMalformedRecentRepeaterSearches) {
|
||||
using mesh::cli::RecentRepeaterGetMatch;
|
||||
mesh::cli::RecentRepeaterGetQuery query;
|
||||
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Invalid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search", query));
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Invalid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search 860", query));
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Invalid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search 8G", query));
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Invalid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters search 860C extra", query));
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::Invalid,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters nope", query));
|
||||
EXPECT_EQ(RecentRepeaterGetMatch::NoMatch,
|
||||
mesh::cli::parseRecentRepeaterGet(
|
||||
"recent.repeaters.extra", query));
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, FormatsRecentRepeaterAgeWithCompactSuffix) {
|
||||
char age[12];
|
||||
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 0);
|
||||
EXPECT_STREQ("0s", age);
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 59);
|
||||
EXPECT_STREQ("59s", age);
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 60);
|
||||
EXPECT_STREQ("1m", age);
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 3599);
|
||||
EXPECT_STREQ("59m", age);
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 3600);
|
||||
EXPECT_STREQ("1h", age);
|
||||
mesh::cli::formatRecentRepeaterAge(age, sizeof(age), 26UL * 3600UL);
|
||||
EXPECT_STREQ("26h", age);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, FormatsUsefulUnknownSettingErrors) {
|
||||
char reply[64] = "";
|
||||
|
||||
|
||||
@@ -233,6 +233,45 @@ TEST(RouteHashPrefixes, MatchesSharedOneTwoOrThreeBytes) {
|
||||
EXPECT_FALSE(routeHashPrefixesOverlap(configured, 3, mismatch, 2));
|
||||
}
|
||||
|
||||
TEST(SimpleMeshTables, RecentRepeaterSearchReturnsAllOverlappingPrefixesInDisplayOrder) {
|
||||
SimpleMeshTables::RecentRepeaterInfo storage[6];
|
||||
SimpleMeshTables t(storage, 6);
|
||||
const uint8_t one_byte[] = {0x86};
|
||||
const uint8_t two_bytes[] = {0x86, 0x0c};
|
||||
const uint8_t three_bytes[] = {0x86, 0x0c, 0xca};
|
||||
const uint8_t other_branch[] = {0x86, 0xd0};
|
||||
const uint8_t unrelated[] = {0x71, 0xce, 0x82};
|
||||
|
||||
ASSERT_TRUE(t.setRecentRepeater(one_byte, 1, 20));
|
||||
ASSERT_TRUE(t.setRecentRepeater(two_bytes, 2, 4));
|
||||
ASSERT_TRUE(t.setRecentRepeater(three_bytes, 3, 8));
|
||||
ASSERT_TRUE(t.setRecentRepeater(other_branch, 2, 12));
|
||||
ASSERT_TRUE(t.setRecentRepeater(unrelated, 3, 16));
|
||||
|
||||
EXPECT_EQ(3, t.getRecentRepeaterMatchingCount(two_bytes, 2));
|
||||
const auto* first = t.getRecentRepeaterMatchingBySortedIdx(two_bytes, 2, 0);
|
||||
const auto* second = t.getRecentRepeaterMatchingBySortedIdx(two_bytes, 2, 1);
|
||||
const auto* third = t.getRecentRepeaterMatchingBySortedIdx(two_bytes, 2, 2);
|
||||
ASSERT_NE(nullptr, first);
|
||||
ASSERT_NE(nullptr, second);
|
||||
ASSERT_NE(nullptr, third);
|
||||
EXPECT_EQ(3, first->prefix_len);
|
||||
EXPECT_EQ(0, memcmp(three_bytes, first->prefix, 3));
|
||||
EXPECT_EQ(2, second->prefix_len);
|
||||
EXPECT_EQ(0, memcmp(two_bytes, second->prefix, 2));
|
||||
EXPECT_EQ(1, third->prefix_len);
|
||||
EXPECT_EQ(0, memcmp(one_byte, third->prefix, 1));
|
||||
EXPECT_EQ(nullptr,
|
||||
t.getRecentRepeaterMatchingBySortedIdx(two_bytes, 2, 3));
|
||||
|
||||
// A one-byte search also includes both two-byte branches sharing it.
|
||||
EXPECT_EQ(4, t.getRecentRepeaterMatchingCount(one_byte, 1));
|
||||
const auto* branch =
|
||||
t.getRecentRepeaterMatchingBySortedIdx(one_byte, 1, 1);
|
||||
ASSERT_NE(nullptr, branch);
|
||||
EXPECT_EQ(0, memcmp(other_branch, branch->prefix, 2));
|
||||
}
|
||||
|
||||
TEST(SimpleMeshTables, ShortFailurePrefixUpdatesOverlappingLongEntry) {
|
||||
SimpleMeshTables::RecentRepeaterInfo storage[MAX_RECENT_REPEATERS];
|
||||
SimpleMeshTables t(storage, MAX_RECENT_REPEATERS);
|
||||
|
||||
Reference in new issue
Block a user