feat: add SD-backed stats archive and dedicated /stats web console
This commit is contained in:
@@ -1,6 +1,129 @@
|
||||
#include "MyMesh.h"
|
||||
#include <algorithm>
|
||||
|
||||
#if defined(ESP32) && WITH_WEB_PANEL
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef ARCHIVE_DEBUG
|
||||
#if defined(MQTT_DEBUG) && MQTT_DEBUG
|
||||
#define ARCHIVE_DEBUG 1
|
||||
#else
|
||||
#define ARCHIVE_DEBUG 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if ARCHIVE_DEBUG
|
||||
#define ARCHIVE_LOG(fmt, ...) Serial.printf("[ARCHIVE] " fmt "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define ARCHIVE_LOG(...) do { } while (0)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr unsigned long kArchiveNeighboursFlushIntervalMs = 60UL * 1000UL;
|
||||
constexpr const char* kArchiveNeighboursSnapshotPath = "/stats/neighbours.snapshot";
|
||||
|
||||
File openArchiveWrite(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
fs->remove(filename);
|
||||
return fs->open(filename, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "w");
|
||||
#else
|
||||
fs->remove(filename);
|
||||
return fs->open(filename, FILE_WRITE);
|
||||
#endif
|
||||
}
|
||||
|
||||
File openArchiveRead(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "r");
|
||||
#else
|
||||
return fs->open(filename, FILE_READ);
|
||||
#endif
|
||||
}
|
||||
|
||||
File openArchiveWriteWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||
if (archive == nullptr) {
|
||||
return File();
|
||||
}
|
||||
FILESYSTEM* fs = archive->getFS();
|
||||
if (fs == nullptr) {
|
||||
return File();
|
||||
}
|
||||
File file = openArchiveWrite(fs, filename);
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
if (!archive->recover()) {
|
||||
return File();
|
||||
}
|
||||
fs = archive->getFS();
|
||||
return fs != nullptr ? openArchiveWrite(fs, filename) : File();
|
||||
}
|
||||
|
||||
File openArchiveReadWithRecovery(ArchiveStorage* archive, const char* filename) {
|
||||
if (archive == nullptr) {
|
||||
return File();
|
||||
}
|
||||
FILESYSTEM* fs = archive->getFS();
|
||||
if (fs == nullptr) {
|
||||
return File();
|
||||
}
|
||||
File file = openArchiveRead(fs, filename);
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
if (!archive->recover()) {
|
||||
return File();
|
||||
}
|
||||
fs = archive->getFS();
|
||||
return fs != nullptr ? openArchiveRead(fs, filename) : File();
|
||||
}
|
||||
|
||||
void escapeJsonString(const char* input, char* output, size_t output_size) {
|
||||
if (output == nullptr || output_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t oi = 0;
|
||||
for (size_t i = 0; input != nullptr && input[i] != 0 && oi + 1 < output_size; ++i) {
|
||||
const char c = input[i];
|
||||
const char* escape = nullptr;
|
||||
switch (c) {
|
||||
case '\\':
|
||||
escape = "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escape = "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
escape = "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escape = "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escape = "\\t";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (escape != nullptr) {
|
||||
while (*escape != 0 && oi + 1 < output_size) {
|
||||
output[oi++] = *escape++;
|
||||
}
|
||||
} else {
|
||||
output[oi++] = c;
|
||||
}
|
||||
}
|
||||
output[oi] = 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
@@ -84,6 +207,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
|
||||
neighbour->advert_timestamp = timestamp;
|
||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
neighbour->snr = (int8_t)(snr * 4);
|
||||
_archive_neighbours_dirty = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -852,12 +976,17 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
{
|
||||
last_millis = 0;
|
||||
_archive = nullptr;
|
||||
uptime_millis = 0;
|
||||
next_archive_neighbours_flush_ms = 0;
|
||||
next_history_sample_ms = 0;
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
_logging = false;
|
||||
_archive_neighbours_dirty = false;
|
||||
region_load_active = false;
|
||||
memset(&_stats_state, 0, sizeof(_stats_state));
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
@@ -912,9 +1041,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
pending_discover_until = 0;
|
||||
}
|
||||
|
||||
void MyMesh::begin(FILESYSTEM *fs) {
|
||||
void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
|
||||
mesh::Mesh::begin();
|
||||
_fs = fs;
|
||||
_archive = archive;
|
||||
last_millis = millis();
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
acl.load(_fs, self_id);
|
||||
@@ -926,10 +1057,45 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
bridge.begin();
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_MQTT_UPLINK)
|
||||
#if defined(ESP_PLATFORM)
|
||||
uint8_t legacy_wifi_powersave = 0;
|
||||
const char* legacy_wifi_ssid = nullptr;
|
||||
const char* legacy_wifi_pwd = nullptr;
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
MQTTPrefs legacy_mqtt_prefs{};
|
||||
MQTTPrefsStore::setDefaults(legacy_mqtt_prefs);
|
||||
MQTTPrefsStore::load(_fs, legacy_mqtt_prefs);
|
||||
legacy_wifi_powersave = legacy_mqtt_prefs.legacy_wifi_powersave;
|
||||
legacy_wifi_ssid = legacy_mqtt_prefs.legacy_wifi_ssid;
|
||||
legacy_wifi_pwd = legacy_mqtt_prefs.legacy_wifi_pwd;
|
||||
#endif
|
||||
network.begin(_fs, legacy_wifi_powersave, legacy_wifi_ssid, legacy_wifi_pwd);
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
board.setInhibitSleep(true);
|
||||
web.setCommandRunner(this);
|
||||
web.setNetworkStateProvider(&network);
|
||||
web.begin(_fs);
|
||||
_stats_history.begin(web.isWebStatsEnabled(), _archive);
|
||||
if (web.isWebStatsEnabled() && _archive != nullptr && _archive->isMounted()) {
|
||||
restoreArchiveNeighbours();
|
||||
next_archive_neighbours_flush_ms = millis() + kArchiveNeighboursFlushIntervalMs;
|
||||
}
|
||||
if (web.isWebStatsEnabled()) {
|
||||
recordStatsEvent(HISTORY_EVENT_BOOT);
|
||||
if (_archive != nullptr) {
|
||||
recordStatsEvent(_archive->isMounted() ? HISTORY_EVENT_ARCHIVE_MOUNTED : HISTORY_EVENT_ARCHIVE_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_MQTT_UPLINK) && !(defined(ESP_PLATFORM) && WITH_WEB_PANEL)
|
||||
board.setInhibitSleep(true);
|
||||
#endif
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
mqtt.setNodeNameSource(_prefs.node_name);
|
||||
mqtt.setWebCommandRunner(this);
|
||||
#if defined(ESP_PLATFORM)
|
||||
mqtt.setNetworkStateProvider(&network);
|
||||
#endif
|
||||
mqtt.begin(_fs);
|
||||
#endif
|
||||
|
||||
@@ -949,6 +1115,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
applyGpsPrefs();
|
||||
#endif
|
||||
|
||||
next_history_sample_ms = futureMillis(1000);
|
||||
}
|
||||
|
||||
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
||||
@@ -1079,6 +1247,7 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
|
||||
NeighbourInfo *neighbour = &neighbours[i];
|
||||
if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) {
|
||||
neighbours[i] = NeighbourInfo(); // clear neighbour entry
|
||||
_archive_neighbours_dirty = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1101,6 +1270,370 @@ void MyMesh::formatMemoryReply(char *reply, size_t reply_size) {
|
||||
StatsFormatHelper::formatMemoryStats(reply, reply_size);
|
||||
}
|
||||
|
||||
size_t MyMesh::getNeighbourCount() const {
|
||||
#if MAX_NEIGHBOURS
|
||||
size_t count = 0;
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
if (neighbours[i].heard_timestamp > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::restoreArchiveNeighbours() {
|
||||
#if MAX_NEIGHBOURS
|
||||
if (_archive == nullptr || !_archive->isMounted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FILESYSTEM* fs = _archive->getFS();
|
||||
if (fs == nullptr || !fs->exists(kArchiveNeighboursSnapshotPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = openArchiveReadWithRecovery(_archive, kArchiveNeighboursSnapshotPath);
|
||||
if (!file) {
|
||||
ARCHIVE_LOG("neighbours restore open failed path=%s", kArchiveNeighboursSnapshotPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
char line[128];
|
||||
size_t line_len = 0;
|
||||
size_t restored = 0;
|
||||
while (file.available()) {
|
||||
const int raw = file.read();
|
||||
if (raw < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const char ch = static_cast<char>(raw);
|
||||
if (ch == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (ch == '\n') {
|
||||
line[line_len] = 0;
|
||||
if (line_len > 0 && restored < MAX_NEIGHBOURS) {
|
||||
char full_hex[65];
|
||||
unsigned long advert_timestamp = 0;
|
||||
unsigned long heard_timestamp = 0;
|
||||
int snr = 0;
|
||||
memset(full_hex, 0, sizeof(full_hex));
|
||||
if (sscanf(line, "%64[^,],%lu,%lu,%d", full_hex, &advert_timestamp, &heard_timestamp, &snr) == 4) {
|
||||
uint8_t pub_key[PUB_KEY_SIZE];
|
||||
if (mesh::Utils::fromHex(pub_key, PUB_KEY_SIZE, full_hex)) {
|
||||
neighbours[restored].id = mesh::Identity(pub_key);
|
||||
neighbours[restored].advert_timestamp = static_cast<uint32_t>(advert_timestamp);
|
||||
neighbours[restored].heard_timestamp = static_cast<uint32_t>(heard_timestamp);
|
||||
neighbours[restored].snr = static_cast<int8_t>(constrain(snr, -128, 127));
|
||||
restored++;
|
||||
}
|
||||
}
|
||||
}
|
||||
line_len = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line_len + 1 < sizeof(line)) {
|
||||
line[line_len++] = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (line_len > 0 && restored < MAX_NEIGHBOURS) {
|
||||
line[line_len] = 0;
|
||||
char full_hex[65];
|
||||
unsigned long advert_timestamp = 0;
|
||||
unsigned long heard_timestamp = 0;
|
||||
int snr = 0;
|
||||
memset(full_hex, 0, sizeof(full_hex));
|
||||
if (sscanf(line, "%64[^,],%lu,%lu,%d", full_hex, &advert_timestamp, &heard_timestamp, &snr) == 4) {
|
||||
uint8_t pub_key[PUB_KEY_SIZE];
|
||||
if (mesh::Utils::fromHex(pub_key, PUB_KEY_SIZE, full_hex)) {
|
||||
neighbours[restored].id = mesh::Identity(pub_key);
|
||||
neighbours[restored].advert_timestamp = static_cast<uint32_t>(advert_timestamp);
|
||||
neighbours[restored].heard_timestamp = static_cast<uint32_t>(heard_timestamp);
|
||||
neighbours[restored].snr = static_cast<int8_t>(constrain(snr, -128, 127));
|
||||
restored++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
_archive_neighbours_dirty = false;
|
||||
return restored > 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::flushArchiveNeighbours() {
|
||||
#if MAX_NEIGHBOURS
|
||||
if (_archive == nullptr || !_archive->isMounted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILESYSTEM* fs = _archive->getFS();
|
||||
if (fs == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
int16_t neighbours_count = 0;
|
||||
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
if (neighbours[i].heard_timestamp > 0) {
|
||||
sorted_neighbours[neighbours_count++] = &neighbours[i];
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->heard_timestamp > b->heard_timestamp;
|
||||
});
|
||||
|
||||
File file = openArchiveWriteWithRecovery(_archive, kArchiveNeighboursSnapshotPath);
|
||||
if (!file) {
|
||||
ARCHIVE_LOG("neighbours open failed path=%s", kArchiveNeighboursSnapshotPath);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total_written = 0;
|
||||
for (int i = 0; i < neighbours_count; ++i) {
|
||||
char full_hex[65];
|
||||
mesh::Utils::toHex(full_hex, sorted_neighbours[i]->id.pub_key, PUB_KEY_SIZE);
|
||||
total_written += file.printf("%s,%lu,%lu,%d\n",
|
||||
full_hex,
|
||||
static_cast<unsigned long>(sorted_neighbours[i]->advert_timestamp),
|
||||
static_cast<unsigned long>(sorted_neighbours[i]->heard_timestamp),
|
||||
static_cast<int>(sorted_neighbours[i]->snr));
|
||||
}
|
||||
file.flush();
|
||||
file.close();
|
||||
ARCHIVE_LOG("neighbours flushed path=%s bytes=%u count=%d",
|
||||
kArchiveNeighboursSnapshotPath,
|
||||
static_cast<unsigned>(total_written),
|
||||
static_cast<int>(neighbours_count));
|
||||
_archive_neighbours_dirty = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::maybeFlushArchiveNeighbours(unsigned long now_ms) {
|
||||
#if MAX_NEIGHBOURS
|
||||
if (!_archive_neighbours_dirty || _archive == nullptr || !_archive->isMounted()) {
|
||||
return;
|
||||
}
|
||||
if (next_archive_neighbours_flush_ms == 0 || millisHasNowPassed(next_archive_neighbours_flush_ms)) {
|
||||
flushArchiveNeighbours();
|
||||
next_archive_neighbours_flush_ms = now_ms + kArchiveNeighboursFlushIntervalMs;
|
||||
}
|
||||
#else
|
||||
(void)now_ms;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::recordStatsEvent(uint8_t type, int16_t value) {
|
||||
_stats_history.recordEvent(type, getRTCClock()->getCurrentTime(), static_cast<uint32_t>(uptime_millis / 1000), value);
|
||||
}
|
||||
|
||||
void MyMesh::updateStatsHistory(unsigned long now_ms) {
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
constexpr uint32_t kLowMemoryEnterBytes = 32UL * 1024UL;
|
||||
constexpr uint32_t kLowMemoryClearBytes = 48UL * 1024UL;
|
||||
constexpr uint32_t kLowMemoryEventCooldownSecs = 5UL * 60UL;
|
||||
_stats_history.setArchive(_archive);
|
||||
_stats_history.setEnabled(web.isWebStatsEnabled());
|
||||
if (!_stats_history.isEnabled()) {
|
||||
_stats_state.initialized = false;
|
||||
_archive_neighbours_dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const bool wifi_connected = network.isWifiConnected();
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
|
||||
#else
|
||||
const bool mqtt_connected = false;
|
||||
#endif
|
||||
const bool web_panel_up = web.isPanelRunning();
|
||||
const bool archive_mounted = (_archive != nullptr) && _archive->isMounted();
|
||||
#if defined(ESP32)
|
||||
const uint32_t free_heap = ESP.getFreeHeap();
|
||||
const uint32_t uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||
bool low_memory = _stats_state.low_memory;
|
||||
if (!_stats_state.initialized) {
|
||||
low_memory = free_heap <= kLowMemoryEnterBytes;
|
||||
} else if (low_memory) {
|
||||
low_memory = free_heap <= kLowMemoryClearBytes;
|
||||
} else {
|
||||
low_memory = free_heap <= kLowMemoryEnterBytes;
|
||||
}
|
||||
#else
|
||||
const bool low_memory = false;
|
||||
#endif
|
||||
|
||||
if (!_stats_state.initialized) {
|
||||
_stats_state.initialized = true;
|
||||
_stats_state.wifi_connected = wifi_connected;
|
||||
_stats_state.mqtt_connected = mqtt_connected;
|
||||
_stats_state.web_panel_up = web_panel_up;
|
||||
_stats_state.archive_mounted = archive_mounted;
|
||||
_stats_state.low_memory = low_memory;
|
||||
_stats_state.last_low_memory_event_uptime_secs = 0;
|
||||
} else {
|
||||
if (_stats_state.mqtt_connected != mqtt_connected) {
|
||||
recordStatsEvent(mqtt_connected ? HISTORY_EVENT_MQTT_CONNECTED : HISTORY_EVENT_MQTT_DISCONNECTED);
|
||||
_stats_state.mqtt_connected = mqtt_connected;
|
||||
}
|
||||
if (_stats_state.web_panel_up != web_panel_up) {
|
||||
recordStatsEvent(web_panel_up ? HISTORY_EVENT_WEB_STARTED : HISTORY_EVENT_WEB_STOPPED);
|
||||
_stats_state.web_panel_up = web_panel_up;
|
||||
}
|
||||
if (_stats_state.archive_mounted != archive_mounted) {
|
||||
recordStatsEvent(archive_mounted ? HISTORY_EVENT_ARCHIVE_MOUNTED : HISTORY_EVENT_ARCHIVE_UNAVAILABLE);
|
||||
_stats_state.archive_mounted = archive_mounted;
|
||||
if (archive_mounted) {
|
||||
if (getNeighbourCount() == 0) {
|
||||
restoreArchiveNeighbours();
|
||||
}
|
||||
next_archive_neighbours_flush_ms = now_ms + kArchiveNeighboursFlushIntervalMs;
|
||||
}
|
||||
}
|
||||
if (!_stats_state.low_memory && low_memory) {
|
||||
#if defined(ESP32)
|
||||
if (_stats_state.last_low_memory_event_uptime_secs == 0 ||
|
||||
(uptime_secs - _stats_state.last_low_memory_event_uptime_secs) >= kLowMemoryEventCooldownSecs) {
|
||||
recordStatsEvent(HISTORY_EVENT_LOW_MEMORY, static_cast<int16_t>(min<uint32_t>(free_heap / 1024, 32767)));
|
||||
_stats_state.last_low_memory_event_uptime_secs = uptime_secs;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
_stats_state.wifi_connected = wifi_connected;
|
||||
_stats_state.low_memory = low_memory;
|
||||
}
|
||||
|
||||
if (next_history_sample_ms == 0 || millisHasNowPassed(next_history_sample_ms)) {
|
||||
HistorySample sample{};
|
||||
sample.epoch_secs = getRTCClock()->getCurrentTime();
|
||||
sample.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||
sample.packets_sent = radio_driver.getPacketsSent();
|
||||
sample.packets_recv = radio_driver.getPacketsRecv();
|
||||
sample.battery_mv = board.getBattMilliVolts();
|
||||
sample.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
|
||||
sample.error_flags = _err_flags;
|
||||
sample.recv_errors = static_cast<uint16_t>(min<uint32_t>(radio_driver.getPacketsRecvErrors(), 0xFFFF));
|
||||
sample.neighbour_count = static_cast<uint16_t>(min<size_t>(getNeighbourCount(), 0xFFFF));
|
||||
sample.direct_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF));
|
||||
sample.flood_dups = static_cast<uint16_t>(min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF));
|
||||
sample.last_rssi_x4 = static_cast<int16_t>(radio_driver.getLastRSSI() * 4.0f);
|
||||
sample.last_snr_x4 = static_cast<int16_t>(radio_driver.getLastSNR() * 4.0f);
|
||||
sample.noise_floor = static_cast<int16_t>(_radio->getNoiseFloor());
|
||||
sample.battery_pct = static_cast<int8_t>(board.getBatteryPercent());
|
||||
#if defined(ESP32)
|
||||
sample.heap_free = ESP.getFreeHeap();
|
||||
sample.heap_min = ESP.getMinFreeHeap();
|
||||
sample.psram_free = ESP.getFreePsram();
|
||||
sample.psram_min = ESP.getMinFreePsram();
|
||||
#endif
|
||||
if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER;
|
||||
if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING;
|
||||
if (board.isVbusPresent()) sample.flags |= HISTORY_FLAG_VBUS;
|
||||
if (wifi_connected) sample.flags |= HISTORY_FLAG_WIFI_CONNECTED;
|
||||
if (mqtt_connected) sample.flags |= HISTORY_FLAG_MQTT_CONNECTED;
|
||||
if (web.isWebEnabled()) sample.flags |= HISTORY_FLAG_WEB_ENABLED;
|
||||
if (web_panel_up) sample.flags |= HISTORY_FLAG_WEB_PANEL_UP;
|
||||
if (archive_mounted) sample.flags |= HISTORY_FLAG_ARCHIVE_MOUNTED;
|
||||
_stats_history.pushSample(sample);
|
||||
next_history_sample_ms = now_ms + 60000UL;
|
||||
}
|
||||
|
||||
_stats_history.maybeFlush(now_ms);
|
||||
maybeFlushArchiveNeighbours(now_ms);
|
||||
#else
|
||||
(void)now_ms;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const {
|
||||
offset += snprintf(&reply[offset], reply_size - offset, "\"events\":[");
|
||||
const size_t max_events = min<size_t>(_stats_history.getEventCount(), 6);
|
||||
const uint32_t now_epoch_secs = getRTCClock()->getCurrentTime();
|
||||
const uint32_t now_uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
|
||||
for (size_t i = 0; i < max_events; ++i) {
|
||||
HistoryEvent event{};
|
||||
if (!_stats_history.getRecentEvent(i, event)) {
|
||||
break;
|
||||
}
|
||||
const uint32_t age_secs = (now_epoch_secs >= event.epoch_secs && event.epoch_secs > 0)
|
||||
? (now_epoch_secs - event.epoch_secs)
|
||||
: ((now_uptime_secs >= event.uptime_secs) ? (now_uptime_secs - event.uptime_secs) : event.uptime_secs);
|
||||
offset += snprintf(&reply[offset], reply_size - offset,
|
||||
"%s{\"t\":%lu,\"type\":\"%s\",\"value\":%d}",
|
||||
i == 0 ? "" : ",",
|
||||
static_cast<unsigned long>(age_secs),
|
||||
StatsHistory::getEventTypeName(event.type),
|
||||
static_cast<int>(event.value));
|
||||
if (offset >= reply_size) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
offset += snprintf(&reply[offset], reply_size - offset, "]");
|
||||
return offset < reply_size;
|
||||
}
|
||||
|
||||
bool MyMesh::appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const {
|
||||
offset += snprintf(&reply[offset], reply_size - offset, "\"neighbors_detail\":[");
|
||||
if (offset >= reply_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
constexpr size_t kMaxNeighboursJson = 10;
|
||||
int16_t neighbours_count = 0;
|
||||
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
auto neighbour = const_cast<NeighbourInfo*>(&neighbours[i]);
|
||||
if (neighbour->heard_timestamp > 0) {
|
||||
sorted_neighbours[neighbours_count++] = neighbour;
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->heard_timestamp > b->heard_timestamp;
|
||||
});
|
||||
|
||||
const size_t emit_count = min<size_t>(neighbours_count, kMaxNeighboursJson);
|
||||
const uint32_t now_secs = getRTCClock()->getCurrentTime();
|
||||
for (size_t i = 0; i < emit_count; ++i) {
|
||||
const NeighbourInfo* neighbour = sorted_neighbours[i];
|
||||
char hex[7];
|
||||
char full_hex[65];
|
||||
mesh::Utils::toHex(hex, neighbour->id.pub_key, 3);
|
||||
mesh::Utils::toHex(full_hex, neighbour->id.pub_key, PUB_KEY_SIZE);
|
||||
const uint32_t heard_secs_ago = now_secs - neighbour->heard_timestamp;
|
||||
const uint32_t advert_secs_ago = now_secs - neighbour->advert_timestamp;
|
||||
ClientInfo* client = const_cast<ClientACL&>(acl).getClient(neighbour->id.pub_key, PUB_KEY_SIZE);
|
||||
const bool route_known = (client != nullptr && client->out_path_len != OUT_PATH_UNKNOWN);
|
||||
offset += snprintf(&reply[offset], reply_size - offset,
|
||||
"%s{\"id\":\"%s\",\"full_id\":\"%s\",\"heard_secs_ago\":%lu,\"advert_secs_ago\":%lu,\"snr_db\":%.2f,\"route\":\"%s\"}",
|
||||
i == 0 ? "" : ",",
|
||||
hex,
|
||||
full_hex,
|
||||
static_cast<unsigned long>(heard_secs_ago),
|
||||
static_cast<unsigned long>(advert_secs_ago),
|
||||
static_cast<double>(neighbour->snr) / 4.0,
|
||||
route_known ? "known" : "unknown");
|
||||
if (offset >= reply_size) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
offset += snprintf(&reply[offset], reply_size - offset, "]");
|
||||
return offset < reply_size;
|
||||
}
|
||||
|
||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
IdentityStore store(*_fs, "");
|
||||
@@ -1307,6 +1840,75 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
sendNodeDiscoverReq();
|
||||
strcpy(reply, "OK - Discover sent");
|
||||
}
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
|
||||
web.formatWebStatusReply(reply, 160);
|
||||
} else if (strcmp(command, "get web.stats.status") == 0) {
|
||||
snprintf(reply, 160,
|
||||
"> enabled:%s history:%s psram:%s degraded:%s samples:%u/%u events:%u/%u archive:%s logical:%s path:%s",
|
||||
web.isWebStatsEnabled() ? "on" : "off",
|
||||
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "active" : "inactive",
|
||||
_stats_history.isPsramBacked() ? "yes" : "no",
|
||||
_stats_history.isDegraded() ? "yes" : "no",
|
||||
static_cast<unsigned>(_stats_history.getSampleCount()),
|
||||
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
||||
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable",
|
||||
(_archive != nullptr) ? _archive->getLogicalName() : "archive",
|
||||
(_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats");
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM)
|
||||
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
||||
network.formatWifiStatusReply(reply, 160);
|
||||
} else if (memcmp(command, "get wifi.ssid", 13) == 0) {
|
||||
sprintf(reply, "> %s", network.getWifiSSID()[0] ? network.getWifiSSID() : "-");
|
||||
} else if (memcmp(command, "get wifi.powersaving", 20) == 0) {
|
||||
sprintf(reply, "> %s", network.getWifiPowerSave());
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
} else if (memcmp(command, "set web ", 8) == 0) {
|
||||
web.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "set.web ", 8) == 0) {
|
||||
web.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "set web.stats ", 14) == 0 || memcmp(command, "set.web.stats ", 15) == 0) {
|
||||
const char* value = (memcmp(command, "set web.stats ", 14) == 0) ? &command[14] : &command[15];
|
||||
const bool enabled = memcmp(value, "on", 2) == 0;
|
||||
if (web.setWebStatsEnabled(enabled)) {
|
||||
_stats_history.setEnabled(enabled);
|
||||
recordStatsEvent(enabled ? HISTORY_EVENT_STATS_ENABLED : HISTORY_EVENT_STATS_DISABLED);
|
||||
if (enabled) {
|
||||
next_history_sample_ms = millis();
|
||||
} else {
|
||||
_stats_state.initialized = false;
|
||||
}
|
||||
strcpy(reply, enabled ? "OK - web.stats on" : "OK - web.stats off");
|
||||
} else {
|
||||
strcpy(reply, "Err - unable to update web.stats");
|
||||
}
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM)
|
||||
} else if (memcmp(command, "set wifi.ssid ", 14) == 0) {
|
||||
if (network.setWifiSSID(&command[14])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - bad wifi.ssid");
|
||||
}
|
||||
} else if (memcmp(command, "set wifi.pwd ", 13) == 0) {
|
||||
if (network.setWifiPassword(&command[13])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - bad wifi.pwd");
|
||||
}
|
||||
} else if (memcmp(command, "set wifi.powersaving ", 21) == 0) {
|
||||
if (network.setWifiPowerSave(&command[21])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - use none|min|max");
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
} else if (memcmp(command, "mqtt.owner ", 11) == 0) {
|
||||
if (mqtt.setOwnerPublicKey(&command[11])) {
|
||||
@@ -1330,14 +1932,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.status") == 0) {
|
||||
mqtt.formatStatusReply(reply, 160);
|
||||
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
|
||||
mqtt.formatWebStatusReply(reply, 160);
|
||||
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
||||
mqtt.formatWifiStatusReply(reply, 160);
|
||||
} else if (memcmp(command, "get wifi.ssid", 13) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.getWifiSSID()[0] ? mqtt.getWifiSSID() : "-");
|
||||
} else if (memcmp(command, "get wifi.powersaving", 20) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.getWifiPowerSave());
|
||||
} else if (memcmp(command, "get mqtt.iata", 13) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.getIata());
|
||||
} else if (memcmp(command, "get mqtt.owner", 14) == 0) {
|
||||
@@ -1359,30 +1953,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
} else if (memcmp(command, "set mqtt.tx ", 12) == 0) {
|
||||
mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "set web ", 8) == 0) {
|
||||
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "set.web ", 8) == 0) {
|
||||
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(command, "set wifi.ssid ", 14) == 0) {
|
||||
if (mqtt.setWifiSSID(&command[14])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - bad wifi.ssid");
|
||||
}
|
||||
} else if (memcmp(command, "set wifi.pwd ", 13) == 0) {
|
||||
if (mqtt.setWifiPassword(&command[13])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - bad wifi.pwd");
|
||||
}
|
||||
} else if (memcmp(command, "set wifi.powersaving ", 21) == 0) {
|
||||
if (mqtt.setWifiPowerSave(&command[21])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - use none|min|max");
|
||||
}
|
||||
} else if (memcmp(command, "set mqtt.iata ", 14) == 0) {
|
||||
if (mqtt.setIata(&command[14])) {
|
||||
strcpy(reply, "OK");
|
||||
@@ -1457,6 +2027,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
matches_exact("clock") ||
|
||||
matches_exact("get mqtt.status") ||
|
||||
matches_exact("get web.status") ||
|
||||
matches_exact("get web.stats.status") ||
|
||||
matches_exact("get web") ||
|
||||
matches_exact("advert") ||
|
||||
matches_exact("reboot") ||
|
||||
@@ -1489,6 +2060,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
matches_exact("get public.key") ||
|
||||
matches_exact("get advert.interval") ||
|
||||
matches_exact("get flood.advert.interval") ||
|
||||
matches_exact("get repeat") ||
|
||||
matches_exact("get flood.max") ||
|
||||
matches_exact("get path.hash.mode") ||
|
||||
matches_exact("get owner.info") ||
|
||||
@@ -1505,6 +2077,8 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
matches_prefix("set mqtt.tx ") ||
|
||||
matches_prefix("set web ") ||
|
||||
matches_prefix("set.web ") ||
|
||||
matches_prefix("set web.stats ") ||
|
||||
matches_prefix("set.web.stats ") ||
|
||||
matches_prefix("set mqtt.eastmesh-au ") ||
|
||||
matches_prefix("set mqtt.eastmesh.au ") ||
|
||||
matches_prefix("set mqtt.letsmesh-eu ") ||
|
||||
@@ -1520,6 +2094,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
matches_prefix("set prv.key ") ||
|
||||
matches_prefix("set advert.interval ") ||
|
||||
matches_prefix("set flood.advert.interval ") ||
|
||||
matches_prefix("set repeat ") ||
|
||||
matches_prefix("set flood.max ") ||
|
||||
matches_prefix("set path.hash.mode ") ||
|
||||
matches_prefix("time ") ||
|
||||
@@ -1538,11 +2113,191 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
reply[reply_size - 1] = 0;
|
||||
}
|
||||
|
||||
bool MyMesh::isWebStatsEnabled() const {
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
return web.isWebStatsEnabled();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
||||
if (reply == nullptr || reply_size == 0) {
|
||||
return false;
|
||||
}
|
||||
reply[0] = 0;
|
||||
|
||||
#if !defined(ESP_PLATFORM) || !WITH_WEB_PANEL
|
||||
return false;
|
||||
#else
|
||||
char wifi_ssid[48];
|
||||
char wifi_status[20];
|
||||
char wifi_ip[20];
|
||||
char wifi_powersave[12];
|
||||
escapeJsonString(network.getWifiSSID()[0] ? network.getWifiSSID() : "-", wifi_ssid, sizeof(wifi_ssid));
|
||||
escapeJsonString(network.getWifiPowerSave(), wifi_powersave, sizeof(wifi_powersave));
|
||||
int wifi_rssi = 0;
|
||||
|
||||
#if defined(ESP32)
|
||||
if (network.getWifiSSID()[0] == 0) {
|
||||
strncpy(wifi_status, "unconfigured", sizeof(wifi_status) - 1);
|
||||
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||
} else if (network.isWifiConnected()) {
|
||||
strncpy(wifi_status, "connected", sizeof(wifi_status) - 1);
|
||||
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||
String ip = WiFi.localIP().toString();
|
||||
escapeJsonString(ip.c_str(), wifi_ip, sizeof(wifi_ip));
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else {
|
||||
strncpy(wifi_status, "connecting", sizeof(wifi_status) - 1);
|
||||
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||
}
|
||||
#else
|
||||
strncpy(wifi_status, "unsupported", sizeof(wifi_status) - 1);
|
||||
wifi_status[sizeof(wifi_status) - 1] = 0;
|
||||
strncpy(wifi_ip, "--", sizeof(wifi_ip) - 1);
|
||||
wifi_ip[sizeof(wifi_ip) - 1] = 0;
|
||||
#endif
|
||||
|
||||
const int battery_pct = board.getBatteryPercent();
|
||||
const bool archive_available = (_archive != nullptr) && _archive->isMounted();
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
|
||||
#else
|
||||
const bool mqtt_connected = false;
|
||||
#endif
|
||||
const bool web_panel_up = web.isPanelRunning();
|
||||
const char* archive_name = (_archive != nullptr) ? _archive->getLogicalName() : "archive";
|
||||
const char* archive_path = (_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats";
|
||||
const char* archive_type = (_archive != nullptr) ? _archive->getCardTypeName() : "unavailable";
|
||||
|
||||
size_t offset = 0;
|
||||
offset += snprintf(&reply[offset], reply_size - offset,
|
||||
"{\"enabled\":true,"
|
||||
"\"history\":{\"active\":%s,\"psram\":%s,\"degraded\":%s,\"samples\":%u,\"sample_capacity\":%u,\"sample_interval_secs\":%lu,"
|
||||
"\"archive_restored\":%s,\"archive_restored_samples\":%u,\"archive_summary_interval_secs\":%lu,"
|
||||
"\"events\":%u,\"event_capacity\":%u},"
|
||||
"\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\","
|
||||
"\"total_bytes\":%llu,\"used_bytes\":%llu},"
|
||||
"\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u,"
|
||||
"\"external_power\":%s,\"charging\":%s,\"vbus\":%s},"
|
||||
"\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu},"
|
||||
"\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,"
|
||||
"\"recv_errors\":%u,\"direct_dups\":%u,\"flood_dups\":%u,\"neighbors\":%u},"
|
||||
"\"memory\":{\"heap_free\":%u,\"heap_min\":%u,\"heap_max\":%u,\"psram_free\":%u,\"psram_min\":%u,\"psram_max\":%u},"
|
||||
"\"wifi\":{\"ssid\":\"%s\",\"status\":\"%s\",\"connected\":%s,\"ip\":\"%s\",\"rssi\":%d,\"powersave\":\"%s\"},"
|
||||
"\"services\":{\"mqtt_connected\":%s,\"web_enabled\":%s,\"web_panel_up\":%s,\"web_auth\":\"%s\","
|
||||
"\"archive_available\":%s}",
|
||||
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "true" : "false",
|
||||
_stats_history.isPsramBacked() ? "true" : "false",
|
||||
_stats_history.isDegraded() ? "true" : "false",
|
||||
static_cast<unsigned>(_stats_history.getSampleCount()),
|
||||
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
||||
static_cast<unsigned long>(StatsHistory::getSampleIntervalSecs()),
|
||||
_stats_history.hasArchiveRestore() ? "true" : "false",
|
||||
static_cast<unsigned>(_stats_history.getRestoredSampleCount()),
|
||||
static_cast<unsigned long>(StatsHistory::getArchiveSummaryIntervalSecs()),
|
||||
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||
archive_name,
|
||||
archive_available ? "true" : "false",
|
||||
archive_path,
|
||||
archive_type,
|
||||
static_cast<unsigned long long>(_archive != nullptr ? _archive->getTotalBytes() : 0),
|
||||
static_cast<unsigned long long>(_archive != nullptr ? _archive->getUsedBytes() : 0),
|
||||
board.getBattMilliVolts(),
|
||||
battery_pct,
|
||||
static_cast<unsigned long>(uptime_millis / 1000),
|
||||
_err_flags,
|
||||
static_cast<unsigned>(_mgr->getOutboundTotal()),
|
||||
board.isExternalPowered() ? "true" : "false",
|
||||
board.isCharging() ? "true" : "false",
|
||||
board.isVbusPresent() ? "true" : "false",
|
||||
static_cast<int>(_radio->getNoiseFloor()),
|
||||
radio_driver.getLastRSSI(),
|
||||
radio_driver.getLastSNR(),
|
||||
static_cast<unsigned long>(getTotalAirTime() / 1000),
|
||||
static_cast<unsigned long>(getReceiveAirTime() / 1000),
|
||||
static_cast<unsigned>(radio_driver.getPacketsRecv()),
|
||||
static_cast<unsigned>(radio_driver.getPacketsSent()),
|
||||
static_cast<unsigned>(getNumSentFlood()),
|
||||
static_cast<unsigned>(getNumSentDirect()),
|
||||
static_cast<unsigned>(getNumRecvFlood()),
|
||||
static_cast<unsigned>(getNumRecvDirect()),
|
||||
static_cast<unsigned>(radio_driver.getPacketsRecvErrors()),
|
||||
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumDirectDups()),
|
||||
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumFloodDups()),
|
||||
static_cast<unsigned>(getNeighbourCount()),
|
||||
ESP.getFreeHeap(),
|
||||
ESP.getMinFreeHeap(),
|
||||
ESP.getMaxAllocHeap(),
|
||||
ESP.getFreePsram(),
|
||||
ESP.getMinFreePsram(),
|
||||
ESP.getMaxAllocPsram(),
|
||||
wifi_ssid,
|
||||
wifi_status,
|
||||
network.isWifiConnected() ? "true" : "false",
|
||||
wifi_ip,
|
||||
wifi_rssi,
|
||||
wifi_powersave,
|
||||
mqtt_connected ? "true" : "false",
|
||||
web.isWebEnabled() ? "true" : "false",
|
||||
web_panel_up ? "true" : "false",
|
||||
web.isPanelUnlocked() ? "unlocked" : "locked",
|
||||
archive_available ? "true" : "false");
|
||||
if (offset >= reply_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += snprintf(&reply[offset], reply_size - offset, ",");
|
||||
if (!appendJsonEvents(reply, reply_size, offset)) {
|
||||
return false;
|
||||
}
|
||||
offset += snprintf(&reply[offset], reply_size - offset, ",");
|
||||
if (!appendJsonNeighbours(reply, reply_size, offset)) {
|
||||
return false;
|
||||
}
|
||||
offset += snprintf(&reply[offset], reply_size - offset, "}");
|
||||
return offset < reply_size;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) {
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
if (!web.isWebStatsEnabled()) {
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
reply[0] = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return _stats_history.buildSeriesJson(
|
||||
series,
|
||||
reply,
|
||||
reply_size,
|
||||
getRTCClock()->getCurrentTime(),
|
||||
static_cast<uint32_t>(uptime_millis / 1000));
|
||||
#else
|
||||
(void)series;
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
reply[0] = 0;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::loop() {
|
||||
#ifdef WITH_BRIDGE
|
||||
bridge.loop();
|
||||
#endif
|
||||
|
||||
const uint32_t now = millis();
|
||||
uptime_millis += now - last_millis;
|
||||
last_millis = now;
|
||||
|
||||
mesh::Mesh::loop();
|
||||
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
@@ -1577,6 +2332,19 @@ void MyMesh::loop() {
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
bool network_required = false;
|
||||
#if WITH_WEB_PANEL
|
||||
network_required = web.isWebEnabled();
|
||||
#endif
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
network_required = network_required || mqtt.isActive();
|
||||
#endif
|
||||
network.loop(network_required);
|
||||
#if WITH_WEB_PANEL
|
||||
web.loop();
|
||||
#endif
|
||||
#endif
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
MQTTStatusSnapshot mqtt_status{};
|
||||
mqtt_status.battery_mv = static_cast<int>(board.getBattMilliVolts());
|
||||
@@ -1593,11 +2361,9 @@ void MyMesh::loop() {
|
||||
mqtt_status.radio_cr = _prefs.cr;
|
||||
mqtt.loop(mqtt_status);
|
||||
#endif
|
||||
|
||||
// update uptime
|
||||
uint32_t now = millis();
|
||||
uptime_millis += now - last_millis;
|
||||
last_millis = now;
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
updateStatsHistory(now);
|
||||
#endif
|
||||
}
|
||||
|
||||
// To check if there is pending work
|
||||
@@ -1607,6 +2373,9 @@ bool MyMesh::hasPendingWork() const {
|
||||
#endif
|
||||
#if defined(WITH_MQTT_UPLINK)
|
||||
if (mqtt.isActive()) return true;
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
if (web.isWebEnabled()) return true;
|
||||
#endif
|
||||
return _mgr->getOutboundTotal() > 0;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,17 @@
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
#include <helpers/mqtt/MQTTUplink.h>
|
||||
#endif
|
||||
#include <helpers/NetworkService.h>
|
||||
#include <helpers/web/WebService.h>
|
||||
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/ArchiveStorage.h>
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/StatsHistory.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/StatsFormatHelper.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
@@ -84,12 +88,16 @@ struct NeighbourInfo {
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public MQTTWebCommandRunner {
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelCommandRunner {
|
||||
FILESYSTEM* _fs;
|
||||
ArchiveStorage* _archive;
|
||||
uint32_t last_millis;
|
||||
uint64_t uptime_millis;
|
||||
unsigned long next_archive_neighbours_flush_ms;
|
||||
unsigned long next_history_sample_ms;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
bool _archive_neighbours_dirty;
|
||||
NodePrefs _prefs;
|
||||
ClientACL acl;
|
||||
CommonCLI _cli;
|
||||
@@ -124,8 +132,32 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public MQTTWebComma
|
||||
#ifdef WITH_MQTT_UPLINK
|
||||
MQTTUplink mqtt;
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM)
|
||||
NetworkService network;
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM) && WITH_WEB_PANEL
|
||||
WebService web;
|
||||
#endif
|
||||
StatsHistory _stats_history;
|
||||
struct {
|
||||
bool initialized;
|
||||
bool wifi_connected;
|
||||
bool mqtt_connected;
|
||||
bool web_panel_up;
|
||||
bool archive_mounted;
|
||||
bool low_memory;
|
||||
uint32_t last_low_memory_event_uptime_secs;
|
||||
} _stats_state;
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
size_t getNeighbourCount() const;
|
||||
void updateStatsHistory(unsigned long now_ms);
|
||||
bool restoreArchiveNeighbours();
|
||||
void flushArchiveNeighbours();
|
||||
void maybeFlushArchiveNeighbours(unsigned long now_ms);
|
||||
void recordStatsEvent(uint8_t type, int16_t value = 0);
|
||||
bool appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const;
|
||||
bool appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const;
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
@@ -182,7 +214,7 @@ protected:
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
|
||||
void begin(FILESYSTEM* fs);
|
||||
void begin(FILESYSTEM* fs, ArchiveStorage* archive = nullptr);
|
||||
void sendNodeDiscoverReq();
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
@@ -224,6 +256,9 @@ public:
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void runWebCommand(const char* command, char* reply, size_t reply_size) override;
|
||||
const char* getWebAdminPassword() const override { return _prefs.password; }
|
||||
bool isWebStatsEnabled() const override;
|
||||
bool formatWebStatsSummaryJson(char* reply, size_t reply_size) override;
|
||||
bool formatWebStatsSeriesJson(const char* series, char* reply, size_t reply_size) override;
|
||||
void loop();
|
||||
|
||||
#if defined(WITH_BRIDGE)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <Mesh.h>
|
||||
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/ArchiveStorage.h>
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
ArchiveStorage archive;
|
||||
|
||||
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
|
||||
|
||||
@@ -33,6 +35,7 @@ void setup() {
|
||||
delay(1000);
|
||||
|
||||
board.begin();
|
||||
archive.begin();
|
||||
|
||||
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
|
||||
// give some extra time for serial to settle so
|
||||
@@ -93,7 +96,7 @@ void setup() {
|
||||
|
||||
sensors.begin();
|
||||
|
||||
the_mesh.begin(fs);
|
||||
the_mesh.begin(fs, &archive);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||
|
||||
مرجع در شماره جدید
Block a user