fix: harden observer boot and T-Beam S3 telemetry
This commit is contained in:
1 parent
7232afc115
commit
cbfe1cec66
18 files changed
+623
-43
No files matched your search
@@ -142,10 +142,12 @@ OK
|
||||
- `get web`
|
||||
- `get web.status`: shows whether the local HTTPS panel is available.
|
||||
- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-active boards also record per-minute satellites samples for the `/stats` history view. If archive access drops while stats remain enabled, the repeater retries the SD mount periodically.
|
||||
- At boot, archive-backed stats restore uses bounded reads of the latest summary, events, and neighbour snapshots so malformed or unexpectedly large archive files do not delay MQTT or web startup.
|
||||
- `set web on|off`
|
||||
- `set.web on|off`: enables or disables the local HTTPS panel.
|
||||
- `set web.stats on|off`
|
||||
- `set.web.stats on|off`: enables or disables the dedicated `/stats` page and historical stats collection.
|
||||
- `purge sd`: deletes files and directories from the mounted SD card archive, then recreates the empty `/stats` archive directory so runtime stats capture can continue. It does not erase the internal repeater filesystem or stored settings.
|
||||
|
||||
### Runtime Diagnostics
|
||||
|
||||
@@ -204,6 +206,8 @@ Notes:
|
||||
- commands run with the same care as if you typed them into the repeater CLI directly
|
||||
- this is intended for local admin use on a trusted network
|
||||
- `start ota` releases the local HTTP redirect listener on port `80` so the OTA HTTP listener can take over without stopping the rest of the repeater services, regardless of whether the command is run from the web panel, serial CLI, or a remote companion/app CLI session
|
||||
- if the old redirect listener has not fully released port `80`, the OTA listener retries for up to about 30 seconds before giving up
|
||||
- the web `Purge SD` button runs `purge sd` after browser confirmation
|
||||
- `start ota` uses the repeater's existing Wi-Fi address when already connected, or starts the `MeshCore-OTA` access point when Wi-Fi is not connected
|
||||
- the `/app` Regions shortcut runs the existing MeshCore region commands in sequence: `region put au`, `region put au-STATE`, `region allowf au`, `region allowf au-STATE`, then `region save`
|
||||
|
||||
|
||||
@@ -348,14 +348,21 @@ On mobile:
|
||||
|
||||
1. Press `Start OTA`.
|
||||
2. Confirm the action.
|
||||
3. The panel starts OTA, waits briefly for the OTA HTTP listener to take over port `80`, then opens the returned `http://.../update` URL.
|
||||
3. The panel starts OTA, waits for the OTA HTTP listener to take over port `80`, then opens the returned `http://.../update` URL.
|
||||
4. The local HTTP redirect listener on port `80` is released so OTA can take over that port.
|
||||
5. Continue with your normal OTA workflow.
|
||||
|
||||
If the repeater is already connected to Wi-Fi, OTA uses the existing LAN address. If Wi-Fi is not connected, the repeater starts the `MeshCore-OTA` access point and returns that OTA address instead.
|
||||
If the old redirect listener has not fully released port `80`, the OTA listener retries for up to about 30 seconds before giving up.
|
||||
|
||||
If an older build sends you through a strange redirect after `start ota`, use the web `Start OTA` button to begin the upgrade. This redirect issue is fixed in `observer-eastmesh-v1.3.11`.
|
||||
|
||||
### Purge SD
|
||||
|
||||
`Purge SD` appears as red inline text next to `Archive Used` in the `/stats` Services card. It asks for confirmation, then runs the authenticated `purge sd` command.
|
||||
|
||||
This deletes files and directories from the mounted SD card archive, then recreates the empty `/stats` archive directory for continued runtime capture. It does not erase the internal repeater filesystem, identity, prefs, regions, radio settings, or MQTT settings.
|
||||
|
||||
### Use Historical Stats
|
||||
|
||||
1. Enable stats if needed with `set web.stats on`.
|
||||
|
||||
@@ -1332,6 +1332,9 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
|
||||
web.setCommandRunner(this);
|
||||
web.setNetworkStateProvider(&network);
|
||||
web.begin(_fs);
|
||||
if (web.isWebEnabled()) {
|
||||
network.loop(true);
|
||||
}
|
||||
_stats_history.begin(web.isWebStatsEnabled(), _archive);
|
||||
if (web.isWebStatsEnabled() && !_stats_history.isLiveOnly() && _archive != nullptr && _archive->isMounted()) {
|
||||
restoreArchiveNeighbours();
|
||||
@@ -1353,6 +1356,11 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
|
||||
mqtt.setNetworkStateProvider(&network);
|
||||
#endif
|
||||
mqtt.begin(_fs);
|
||||
#if defined(ESP_PLATFORM)
|
||||
if (mqtt.isActive()) {
|
||||
network.loop(true);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
@@ -1578,6 +1586,41 @@ void MyMesh::formatMemoryReply(char *reply, size_t reply_size) {
|
||||
StatsFormatHelper::formatMemoryStats(reply, reply_size);
|
||||
}
|
||||
|
||||
bool MyMesh::purgeSdCard(char* reply, size_t reply_size) {
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
reply[0] = 0;
|
||||
}
|
||||
if (_archive == nullptr || !_archive->isSupported()) {
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
snprintf(reply, reply_size, "Err - SD archive unsupported");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!_archive->isMounted() && !_archive->recover()) {
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
snprintf(reply, reply_size, "Err - SD archive unavailable");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t files_removed = 0;
|
||||
uint32_t dirs_removed = 0;
|
||||
if (!_archive->purge(&files_removed, &dirs_removed)) {
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
snprintf(reply, reply_size, "Err - SD purge failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_archive_neighbours_dirty = true;
|
||||
if (reply != nullptr && reply_size > 0) {
|
||||
snprintf(reply, reply_size, "OK - SD purged files:%lu dirs:%lu",
|
||||
static_cast<unsigned long>(files_removed),
|
||||
static_cast<unsigned long>(dirs_removed));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t MyMesh::getNeighbourCount() const {
|
||||
#if MAX_NEIGHBOURS
|
||||
size_t count = 0;
|
||||
@@ -1618,10 +1661,21 @@ bool MyMesh::restoreArchiveNeighbours() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t max_restore_bytes = static_cast<size_t>(MAX_NEIGHBOURS) * 128U;
|
||||
const size_t file_size = static_cast<size_t>(file.size());
|
||||
if (file_size > max_restore_bytes) {
|
||||
const size_t start = file_size - max_restore_bytes;
|
||||
if (!file.seek(start)) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
char line[128];
|
||||
size_t line_len = 0;
|
||||
size_t restored = 0;
|
||||
bool skip_partial = file_size > max_restore_bytes;
|
||||
while (file.available()) {
|
||||
const int raw = file.read();
|
||||
if (raw < 0) {
|
||||
@@ -1629,6 +1683,12 @@ bool MyMesh::restoreArchiveNeighbours() {
|
||||
}
|
||||
|
||||
const char ch = static_cast<char>(raw);
|
||||
if (skip_partial) {
|
||||
if (ch == '\n') {
|
||||
skip_partial = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch == '\r') {
|
||||
continue;
|
||||
}
|
||||
@@ -2211,6 +2271,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable");
|
||||
} else if (strcmp(command, "purge sd") == 0) {
|
||||
purgeSdCard(reply, 160);
|
||||
#endif
|
||||
#if defined(TBEAM_1W)
|
||||
} else if (strcmp(command, "get fan") == 0) {
|
||||
|
||||
@@ -297,6 +297,7 @@ public:
|
||||
void formatRadioStatsReply(char *reply, size_t reply_size) override;
|
||||
void formatPacketStatsReply(char *reply, size_t reply_size) override;
|
||||
void formatMemoryReply(char *reply, size_t reply_size) override;
|
||||
bool purgeSdCard(char* reply, size_t reply_size);
|
||||
void prepareForOTAStart() override;
|
||||
void startRegionsLoad() override;
|
||||
bool saveRegions() override;
|
||||
|
||||
@@ -34,6 +34,9 @@ void setup() {
|
||||
delay(1000);
|
||||
|
||||
board.begin();
|
||||
|
||||
sensors.begin();
|
||||
|
||||
archive.begin();
|
||||
|
||||
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
|
||||
@@ -90,8 +93,6 @@ void setup() {
|
||||
|
||||
command[0] = 0;
|
||||
|
||||
sensors.begin();
|
||||
|
||||
the_mesh.begin(fs, &archive);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -152,6 +152,33 @@ releases:
|
||||
text: "Updated EastMesh custom CLI docs for the new `flood.max.advert` command."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh
|
||||
version: "2026.6.2"
|
||||
tag: "observer-eastmesh-v2026.6.2"
|
||||
date: "2026-06-07"
|
||||
previous_version: "2026.6.1"
|
||||
summary: "Improves Observer boot reliability, T-Beam S3 Supreme telemetry, Wi-Fi reconnect speed, and web-panel archive maintenance."
|
||||
changes:
|
||||
- type: fixed
|
||||
area: board
|
||||
text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and telemetry reads by using the board-detected address and a stop-based I2C read path."
|
||||
- type: fixed
|
||||
area: wifi
|
||||
text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier and remembering the last connected AP channel, with automatic fallback to full scan if that channel is stale."
|
||||
- type: fixed
|
||||
area: cli
|
||||
text: "Fixed `set wifi.powersaving none` returning an error when Wi-Fi power saving was already disabled."
|
||||
- type: fixed
|
||||
area: archive
|
||||
text: "Bounded archive neighbour restore and history reads so large SD-card archive files no longer slow boot unnecessarily."
|
||||
- type: added
|
||||
area: web
|
||||
text: "Added a web-panel and CLI SD purge action for clearing archive contents from supported SD-card targets."
|
||||
- type: fixed
|
||||
area: web
|
||||
text: "Hardened web-panel OTA startup when the OTA listener port is still being released."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh-bridge-espnow
|
||||
version: "2026.5.1"
|
||||
tag: "observer-eastmesh-bridge-espnow-v2026.5.1"
|
||||
@@ -215,3 +242,30 @@ releases:
|
||||
area: docs
|
||||
text: "Updated EastMesh custom CLI docs for the new `flood.max.advert` command."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh-bridge-espnow
|
||||
version: "2026.6.2"
|
||||
tag: "observer-eastmesh-bridge-espnow-v2026.6.2"
|
||||
date: "2026-06-07"
|
||||
previous_version: "2026.6.1"
|
||||
summary: "Applies the Observer boot, Wi-Fi, telemetry, archive, and web-panel maintenance fixes to ESP-NOW bridge builds."
|
||||
changes:
|
||||
- type: fixed
|
||||
area: board
|
||||
text: "Fixed LilyGo T-Beam S3 Supreme BME280 detection and telemetry reads by using the board-detected address and a stop-based I2C read path."
|
||||
- type: fixed
|
||||
area: wifi
|
||||
text: "Improved boot-time Wi-Fi startup by starting Wi-Fi earlier and remembering the last connected AP channel, with automatic fallback to full scan if that channel is stale."
|
||||
- type: fixed
|
||||
area: cli
|
||||
text: "Fixed `set wifi.powersaving none` returning an error when Wi-Fi power saving was already disabled."
|
||||
- type: fixed
|
||||
area: archive
|
||||
text: "Bounded archive neighbour restore and history reads so large SD-card archive files no longer slow boot unnecessarily."
|
||||
- type: added
|
||||
area: web
|
||||
text: "Added a web-panel and CLI SD purge action for clearing archive contents from supported SD-card targets."
|
||||
- type: fixed
|
||||
area: web
|
||||
text: "Hardened web-panel OTA startup when the OTA listener port is still being released."
|
||||
breaking_changes: []
|
||||
@@ -48,6 +48,52 @@ void deassertChipSelect(int pin, uint8_t archive_cs_pin) {
|
||||
pinMode(static_cast<uint8_t>(pin), OUTPUT);
|
||||
digitalWrite(static_cast<uint8_t>(pin), HIGH);
|
||||
}
|
||||
|
||||
bool removeArchivePath(FILESYSTEM* fs, const char* path, uint8_t depth, uint32_t& files_removed, uint32_t& dirs_removed) {
|
||||
if (fs == nullptr || path == nullptr || path[0] == 0 || depth > 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File entry = fs->open(path, FILE_READ);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entry.isDirectory()) {
|
||||
entry.close();
|
||||
if (fs->remove(path)) {
|
||||
files_removed++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
while (true) {
|
||||
File child = entry.openNextFile(FILE_READ);
|
||||
if (!child) {
|
||||
break;
|
||||
}
|
||||
|
||||
char child_path[160];
|
||||
const int written = snprintf(child_path, sizeof(child_path), "%s", child.path());
|
||||
child.close();
|
||||
if (written <= 0 || static_cast<size_t>(written) >= sizeof(child_path) ||
|
||||
!removeArchivePath(fs, child_path, depth + 1, files_removed, dirs_removed)) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
entry.close();
|
||||
|
||||
if (strcmp(path, "/") != 0) {
|
||||
if (fs->rmdir(path)) {
|
||||
dirs_removed++;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
@@ -214,6 +260,42 @@ FILESYSTEM* ArchiveStorage::getFS() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ArchiveStorage::purge(uint32_t* files_removed, uint32_t* dirs_removed) {
|
||||
#if defined(ESP32)
|
||||
if (!_mounted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
prepareBusForArchive();
|
||||
uint32_t removed_files = 0;
|
||||
uint32_t removed_dirs = 0;
|
||||
const bool ok = removeArchivePath(&SD, "/", 0, removed_files, removed_dirs);
|
||||
if (!SD.exists(getFsStatsPath())) {
|
||||
SD.mkdir(getFsStatsPath());
|
||||
}
|
||||
refreshCardInfo();
|
||||
if (files_removed != nullptr) {
|
||||
*files_removed = removed_files;
|
||||
}
|
||||
if (dirs_removed != nullptr) {
|
||||
*dirs_removed = removed_dirs;
|
||||
}
|
||||
ARCHIVE_LOG("purged files=%lu dirs=%lu ok=%s",
|
||||
static_cast<unsigned long>(removed_files),
|
||||
static_cast<unsigned long>(removed_dirs),
|
||||
ok ? "yes" : "no");
|
||||
return ok;
|
||||
#else
|
||||
if (files_removed != nullptr) {
|
||||
*files_removed = 0;
|
||||
}
|
||||
if (dirs_removed != nullptr) {
|
||||
*dirs_removed = 0;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* ArchiveStorage::getCardTypeName() const {
|
||||
#if defined(ESP32)
|
||||
if (!_mounted) {
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
bool isAttempted() const { return _attempted; }
|
||||
bool isMounted() const { return _mounted; }
|
||||
bool hadMountFailure() const { return _mount_failed; }
|
||||
bool purge(uint32_t* files_removed = nullptr, uint32_t* dirs_removed = nullptr);
|
||||
|
||||
const char* getLogicalName() const { return "archive"; }
|
||||
const char* getLogicalStatsPath() const { return "archive:/stats"; }
|
||||
|
||||
@@ -15,9 +15,12 @@ AsyncWebServer* ota_server = nullptr;
|
||||
char ota_id_buf[60];
|
||||
char ota_home_buf[90];
|
||||
volatile bool ota_start_pending = false;
|
||||
constexpr uint8_t kOtaStartAttempts = 60;
|
||||
constexpr uint32_t kOtaStartRetryDelayMs = 500;
|
||||
|
||||
void startOTAServerNow() {
|
||||
bool startOTAServerNow() {
|
||||
if (ota_server != nullptr) {
|
||||
ota_server->end();
|
||||
delete ota_server;
|
||||
ota_server = nullptr;
|
||||
}
|
||||
@@ -34,11 +37,28 @@ void startOTAServerNow() {
|
||||
AsyncElegantOTA.setID(ota_id_buf);
|
||||
AsyncElegantOTA.begin(ota_server); // Start ElegantOTA
|
||||
ota_server->begin();
|
||||
if (ota_server->state() != LISTEN) {
|
||||
ota_server->end();
|
||||
delete ota_server;
|
||||
ota_server = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void startOTAServerTask(void*) {
|
||||
vTaskDelay(pdMS_TO_TICKS(750));
|
||||
startOTAServerNow();
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
for (uint8_t attempt = 1; attempt <= kOtaStartAttempts; ++attempt) {
|
||||
if (startOTAServerNow()) {
|
||||
MESH_DEBUG_PRINTLN("OTA server listening on port 80");
|
||||
ota_start_pending = false;
|
||||
vTaskDelete(nullptr);
|
||||
return;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("OTA server port 80 busy, retry %u/%u", attempt, kOtaStartAttempts);
|
||||
vTaskDelay(pdMS_TO_TICKS(kOtaStartRetryDelayMs));
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("OTA server failed to bind port 80");
|
||||
ota_start_pending = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
@@ -68,8 +88,12 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
if (!ota_start_pending) {
|
||||
ota_start_pending = true;
|
||||
if (xTaskCreate(startOTAServerTask, "ota-start", 4096, nullptr, 1, nullptr) != pdPASS) {
|
||||
startOTAServerNow();
|
||||
const bool started = startOTAServerNow();
|
||||
ota_start_pending = false;
|
||||
if (!started) {
|
||||
strcpy(reply, "Error - OTA listener start failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ void NetworkPrefsStore::setDefaults(NetworkPrefs& prefs) {
|
||||
memset(&prefs, 0, sizeof(prefs));
|
||||
prefs.magic = kMagic;
|
||||
prefs.wifi_powersave = 0;
|
||||
prefs.wifi_channel = 0;
|
||||
}
|
||||
|
||||
bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs,
|
||||
@@ -102,6 +103,9 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs,
|
||||
if (prefs.wifi_powersave > 2) {
|
||||
prefs.wifi_powersave = 0;
|
||||
}
|
||||
if (prefs.wifi_channel < 1 || prefs.wifi_channel > 14) {
|
||||
prefs.wifi_channel = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
struct NetworkPrefs {
|
||||
uint32_t magic;
|
||||
uint8_t wifi_powersave;
|
||||
uint8_t reserved[3];
|
||||
uint8_t wifi_channel;
|
||||
uint8_t reserved[2];
|
||||
char wifi_ssid[33];
|
||||
char wifi_pwd[65];
|
||||
};
|
||||
|
||||
@@ -14,8 +14,13 @@ namespace {
|
||||
#if defined(ESP_PLATFORM)
|
||||
constexpr unsigned long kWifiRetryMillis = 15000;
|
||||
constexpr unsigned long kWifiConnectTimeoutMillis = 45000;
|
||||
constexpr unsigned long kWifiChannelHintTimeoutMillis = 7000;
|
||||
constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z
|
||||
|
||||
bool isValidWifiChannel(uint8_t channel) {
|
||||
return channel >= 1 && channel <= 14;
|
||||
}
|
||||
|
||||
int getWifiQualityPercent(int rssi_dbm) {
|
||||
if (rssi_dbm <= -100) {
|
||||
return 0;
|
||||
@@ -43,7 +48,7 @@ const char* getWifiQualityLabel(int rssi_dbm) {
|
||||
} // namespace
|
||||
|
||||
NetworkService::NetworkService()
|
||||
: _fs(nullptr), _prefs{}, _wifi_started(false), _sntp_started(false), _have_time_sync(false), _last_wifi_attempt(0) {
|
||||
: _fs(nullptr), _prefs{}, _wifi_started(false), _sntp_started(false), _have_time_sync(false), _last_wifi_status(-1), _last_wifi_attempt(0) {
|
||||
NetworkPrefsStore::setDefaults(_prefs);
|
||||
}
|
||||
|
||||
@@ -53,6 +58,12 @@ void NetworkService::begin(FILESYSTEM* fs,
|
||||
const char* legacy_wifi_pwd) {
|
||||
_fs = fs;
|
||||
NetworkPrefsStore::load(_fs, _prefs, legacy_wifi_powersave, legacy_wifi_ssid, legacy_wifi_pwd);
|
||||
#if defined(ESP_PLATFORM)
|
||||
Serial.printf("[BOOT] wifi prefs powersave=%s channel=%u ssid=%s\n",
|
||||
getPowerSaveLabel(_prefs.wifi_powersave),
|
||||
_prefs.wifi_channel,
|
||||
_prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "-");
|
||||
#endif
|
||||
}
|
||||
|
||||
void NetworkService::end() {
|
||||
@@ -65,6 +76,7 @@ void NetworkService::end() {
|
||||
_wifi_started = false;
|
||||
_sntp_started = false;
|
||||
_have_time_sync = false;
|
||||
_last_wifi_status = -1;
|
||||
_last_wifi_attempt = 0;
|
||||
}
|
||||
|
||||
@@ -86,6 +98,9 @@ bool NetworkService::setWifiSSID(const char* ssid) {
|
||||
return false;
|
||||
}
|
||||
StrHelper::strncpy(_prefs.wifi_ssid, ssid, sizeof(_prefs.wifi_ssid));
|
||||
#if defined(ESP_PLATFORM)
|
||||
_prefs.wifi_channel = 0;
|
||||
#endif
|
||||
bool ok = savePrefs();
|
||||
reconnectWifi();
|
||||
return ok;
|
||||
@@ -96,6 +111,9 @@ bool NetworkService::setWifiPassword(const char* pwd) {
|
||||
return false;
|
||||
}
|
||||
StrHelper::strncpy(_prefs.wifi_pwd, pwd, sizeof(_prefs.wifi_pwd));
|
||||
#if defined(ESP_PLATFORM)
|
||||
_prefs.wifi_channel = 0;
|
||||
#endif
|
||||
bool ok = savePrefs();
|
||||
reconnectWifi();
|
||||
return ok;
|
||||
@@ -106,22 +124,41 @@ bool NetworkService::setWifiPowerSave(const char* mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char normalized[8];
|
||||
size_t len = 0;
|
||||
while (*mode == ' ' || *mode == '\t') {
|
||||
mode++;
|
||||
}
|
||||
while (len < sizeof(normalized) - 1) {
|
||||
char c = mode[len];
|
||||
if (c == 0 || c == '\r' || c == '\n' || c == ' ' || c == '\t') {
|
||||
break;
|
||||
}
|
||||
normalized[len] = c;
|
||||
len++;
|
||||
}
|
||||
normalized[len] = 0;
|
||||
|
||||
uint8_t next_mode;
|
||||
if (strcmp(mode, "none") == 0) {
|
||||
if (strcmp(normalized, "none") == 0) {
|
||||
next_mode = 0;
|
||||
} else if (strcmp(mode, "min") == 0) {
|
||||
} else if (strcmp(normalized, "min") == 0) {
|
||||
next_mode = 1;
|
||||
} else if (strcmp(mode, "max") == 0) {
|
||||
} else if (strcmp(normalized, "max") == 0) {
|
||||
next_mode = 2;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_prefs.wifi_powersave == next_mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_prefs.wifi_powersave = next_mode;
|
||||
bool ok = savePrefs();
|
||||
#if defined(ESP_PLATFORM)
|
||||
if (_wifi_started) {
|
||||
ok = WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave)) && ok;
|
||||
WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave));
|
||||
}
|
||||
#endif
|
||||
return ok;
|
||||
@@ -244,6 +281,7 @@ void NetworkService::ensureWifi(bool network_required) {
|
||||
_wifi_started = false;
|
||||
_sntp_started = false;
|
||||
_have_time_sync = false;
|
||||
_last_wifi_status = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -253,13 +291,42 @@ void NetworkService::ensureWifi(bool network_required) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
wl_status_t status = WiFi.status();
|
||||
if (static_cast<int>(status) != _last_wifi_status) {
|
||||
_last_wifi_status = static_cast<int>(status);
|
||||
if (status == WL_CONNECTED) {
|
||||
const int connected_channel = WiFi.channel();
|
||||
Serial.printf("[BOOT] wifi connected t=%lu ip=%s rssi=%d channel=%d\n",
|
||||
static_cast<unsigned long>(millis()),
|
||||
WiFi.localIP().toString().c_str(),
|
||||
WiFi.RSSI(),
|
||||
connected_channel);
|
||||
if (connected_channel > 0 && connected_channel <= 14 && _prefs.wifi_channel != connected_channel) {
|
||||
_prefs.wifi_channel = static_cast<uint8_t>(connected_channel);
|
||||
Serial.printf("[BOOT] wifi learned channel=%u save=%s\n",
|
||||
_prefs.wifi_channel,
|
||||
savePrefs() ? "ok" : "failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status == WL_CONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long now_ms = millis();
|
||||
wl_status_t status = WiFi.status();
|
||||
if (_wifi_started) {
|
||||
if (isValidWifiChannel(_prefs.wifi_channel) &&
|
||||
_last_wifi_attempt != 0 &&
|
||||
now_ms - _last_wifi_attempt >= kWifiChannelHintTimeoutMillis) {
|
||||
Serial.printf("[BOOT] wifi channel hint timeout t=%lu channel=%u\n",
|
||||
static_cast<unsigned long>(millis()),
|
||||
_prefs.wifi_channel);
|
||||
_prefs.wifi_channel = 0;
|
||||
savePrefs();
|
||||
WiFi.disconnect(false, false);
|
||||
_last_wifi_attempt = 0;
|
||||
}
|
||||
if (_last_wifi_attempt != 0 && status == WL_IDLE_STATUS && now_ms - _last_wifi_attempt < kWifiConnectTimeoutMillis) {
|
||||
return;
|
||||
}
|
||||
@@ -272,10 +339,17 @@ void NetworkService::ensureWifi(bool network_required) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setSleep(toEspPowerSave(_prefs.wifi_powersave));
|
||||
_wifi_started = true;
|
||||
Serial.printf("[BOOT] wifi start t=%lu\n", static_cast<unsigned long>(millis()));
|
||||
}
|
||||
|
||||
_last_wifi_attempt = now_ms;
|
||||
WiFi.begin(_prefs.wifi_ssid, _prefs.wifi_pwd);
|
||||
if (isValidWifiChannel(_prefs.wifi_channel)) {
|
||||
WiFi.begin(_prefs.wifi_ssid, _prefs.wifi_pwd, _prefs.wifi_channel);
|
||||
Serial.printf("[BOOT] wifi begin t=%lu channel=%u\n", static_cast<unsigned long>(millis()), _prefs.wifi_channel);
|
||||
} else {
|
||||
WiFi.begin(_prefs.wifi_ssid, _prefs.wifi_pwd);
|
||||
Serial.printf("[BOOT] wifi begin t=%lu channel=scan\n", static_cast<unsigned long>(millis()));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkService::updateTimeSync() {
|
||||
|
||||
@@ -46,5 +46,6 @@ private:
|
||||
bool _wifi_started;
|
||||
bool _sntp_started;
|
||||
bool _have_time_sync;
|
||||
int _last_wifi_status;
|
||||
unsigned long _last_wifi_attempt;
|
||||
};
|
||||
@@ -783,6 +783,7 @@ bool StatsHistory::restoreSummaryLog() {
|
||||
if (latest_file) {
|
||||
char line[384];
|
||||
size_t line_len = 0;
|
||||
bool line_too_long = false;
|
||||
while (latest_file.available()) {
|
||||
const int raw = latest_file.read();
|
||||
if (raw < 0) {
|
||||
@@ -797,10 +798,17 @@ bool StatsHistory::restoreSummaryLog() {
|
||||
}
|
||||
if (line_len + 1 < sizeof(line)) {
|
||||
line[line_len++] = ch;
|
||||
} else {
|
||||
line_too_long = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
line[line_len] = 0;
|
||||
latest_file.close();
|
||||
if (line_too_long) {
|
||||
ARCHIVE_LOG("summary latest ignored: line too long path=%s", kSummaryLatestPath);
|
||||
return false;
|
||||
}
|
||||
HistorySample latest_sample{};
|
||||
if (line_len > 0 && parseSummaryLine(line, latest_sample)) {
|
||||
storeSample(latest_sample, false);
|
||||
@@ -932,9 +940,10 @@ bool StatsHistory::restoreEventsLog() {
|
||||
}
|
||||
|
||||
const size_t size = static_cast<size_t>(file.size());
|
||||
const size_t start = (strcmp(events_restore_path, kEventsLatestPath) == 0)
|
||||
? 0
|
||||
: ((size > kEventsRestoreWindowBytes) ? (size - kEventsRestoreWindowBytes) : 0);
|
||||
const bool is_latest_events = strcmp(events_restore_path, kEventsLatestPath) == 0;
|
||||
const size_t latest_events_window = _event_capacity * 160U;
|
||||
const size_t restore_window = is_latest_events ? latest_events_window : kEventsRestoreWindowBytes;
|
||||
const size_t start = (size > restore_window) ? (size - restore_window) : 0;
|
||||
if (start > 0 && !file.seek(start)) {
|
||||
file.close();
|
||||
return false;
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
//#include <RadioLib.h>
|
||||
|
||||
uint32_t deviceOnline = 0x00;
|
||||
static uint8_t detectedBME280Address = 0;
|
||||
|
||||
uint8_t boardDetectedI2CSensorAddress(const char* name) {
|
||||
#ifdef MESH_DEBUG
|
||||
if (name != nullptr && strcmp(name, "BME280") == 0 && (deviceOnline & (1UL << 7))) {
|
||||
return detectedBME280Address;
|
||||
}
|
||||
#endif
|
||||
(void)name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool pmuInterrupt;
|
||||
static void setPmuFlag()
|
||||
@@ -40,6 +51,19 @@ void TBeamBoard::begin() {
|
||||
}
|
||||
|
||||
#ifdef MESH_DEBUG
|
||||
static uint8_t readI2CRegister8(TwoWire* w, uint8_t addr, uint8_t reg)
|
||||
{
|
||||
w->beginTransmission(addr);
|
||||
w->write(reg);
|
||||
if (w->endTransmission(false) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (w->requestFrom((uint8_t)addr, (uint8_t)1) != 1) {
|
||||
return 0;
|
||||
}
|
||||
return w->read();
|
||||
}
|
||||
|
||||
void TBeamBoard::scanDevices(TwoWire *w)
|
||||
{
|
||||
uint8_t err, addr;
|
||||
@@ -56,8 +80,15 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
switch (addr) {
|
||||
case 0x77:
|
||||
case 0x76:
|
||||
Serial.println("\tFound BME280 Sensor");
|
||||
deviceOnline |= BME280_ONLINE;
|
||||
{
|
||||
const uint8_t chipId = readI2CRegister8(w, addr, 0xD0);
|
||||
if (chipId == 0x60) {
|
||||
Serial.print("\tFound BME280 Sensor at address 0x");
|
||||
Serial.println(addr, HEX);
|
||||
detectedBME280Address = addr;
|
||||
deviceOnline |= BME280_ONLINE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x34:
|
||||
Serial.println("\tFound AXP192/AXP2101 PMU");
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#define P_BOARD_RTC_INT 14 //RTC Int pin
|
||||
|
||||
//I2C Wire addresses
|
||||
#define I2C_BME280_ADD 0x76 //BME280 sensor I2C address on Wire
|
||||
#define I2C_BME280_ADD 0x77 //BME280 sensor I2C address on Wire
|
||||
#define I2C_OLED_ADD 0x3C //SH1106 OLED I2C address on Wire
|
||||
#define I2C_QMC6310U_ADD 0x1C //QMC6310U mag sensor I2C address on Wire
|
||||
|
||||
|
||||
@@ -65,9 +65,11 @@ static Adafruit_AHTX0 AHTX0;
|
||||
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
|
||||
#endif
|
||||
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
|
||||
#ifndef TBEAM_SUPREME_SX1262
|
||||
#include <Adafruit_BME280.h>
|
||||
static Adafruit_BME280 BME280;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
#ifndef TELEM_BMP280_ADDRESS
|
||||
@@ -211,17 +213,71 @@ static RAK12500LocationProvider RAK12500_provider;
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// I2C bus scanner
|
||||
// Probes every valid address and records which ones ACK.
|
||||
// I2C sensor probe
|
||||
// Probes only the compiled-in sensor addresses.
|
||||
// This runs before any sensor library is touched, so a missing
|
||||
// or misbehaving device cannot stall or crash the boot sequence.
|
||||
// ============================================================
|
||||
|
||||
static void scanI2CBus(TwoWire* wire, bool found[128]) {
|
||||
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
|
||||
wire->beginTransmission(addr);
|
||||
found[addr] = (wire->endTransmission() == 0);
|
||||
static bool probeI2CAddress(TwoWire* wire, uint8_t addr) {
|
||||
if (wire == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#if defined(ESP32)
|
||||
wire->setTimeOut(20);
|
||||
#endif
|
||||
wire->beginTransmission(addr);
|
||||
return wire->endTransmission() == 0;
|
||||
}
|
||||
|
||||
static bool readI2CRegister8(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t* value) {
|
||||
if (wire == nullptr || value == nullptr) {
|
||||
return false;
|
||||
}
|
||||
wire->beginTransmission(addr);
|
||||
wire->write(reg);
|
||||
if (wire->endTransmission(true) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (wire->requestFrom((uint8_t)addr, (uint8_t)1) != 1) {
|
||||
return false;
|
||||
}
|
||||
*value = wire->read();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool readI2CBuffer(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t* buffer, size_t len) {
|
||||
if (wire == nullptr || buffer == nullptr || len == 0) {
|
||||
return false;
|
||||
}
|
||||
wire->beginTransmission(addr);
|
||||
wire->write(reg);
|
||||
if (wire->endTransmission(true) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (wire->requestFrom((uint8_t)addr, (uint8_t)len) != len) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
buffer[i] = wire->read();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeI2CRegister8(TwoWire* wire, uint8_t addr, uint8_t reg, uint8_t value) {
|
||||
if (wire == nullptr) {
|
||||
return false;
|
||||
}
|
||||
wire->beginTransmission(addr);
|
||||
wire->write(reg);
|
||||
wire->write(value);
|
||||
return wire->endTransmission(true) == 0;
|
||||
}
|
||||
|
||||
uint8_t boardDetectedI2CSensorAddress(const char* name) __attribute__((weak));
|
||||
uint8_t boardDetectedI2CSensorAddress(const char* name) {
|
||||
(void)name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -269,8 +325,151 @@ static void query_bme680(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
#ifdef TBEAM_SUPREME_SX1262
|
||||
struct BME280Calibration {
|
||||
uint16_t dig_T1;
|
||||
int16_t dig_T2;
|
||||
int16_t dig_T3;
|
||||
uint16_t dig_P1;
|
||||
int16_t dig_P2;
|
||||
int16_t dig_P3;
|
||||
int16_t dig_P4;
|
||||
int16_t dig_P5;
|
||||
int16_t dig_P6;
|
||||
int16_t dig_P7;
|
||||
int16_t dig_P8;
|
||||
int16_t dig_P9;
|
||||
uint8_t dig_H1;
|
||||
int16_t dig_H2;
|
||||
uint8_t dig_H3;
|
||||
int16_t dig_H4;
|
||||
int16_t dig_H5;
|
||||
int8_t dig_H6;
|
||||
};
|
||||
|
||||
static BME280Calibration BME280Cal;
|
||||
static TwoWire* BME280Wire = nullptr;
|
||||
static uint8_t BME280Address = 0;
|
||||
static int32_t BME280TFine = 0;
|
||||
|
||||
static uint16_t u16le(const uint8_t* p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
|
||||
static int16_t s16le(const uint8_t* p) {
|
||||
return (int16_t)u16le(p);
|
||||
}
|
||||
|
||||
static bool read_bme280_calibration(TwoWire* wire, uint8_t addr) {
|
||||
uint8_t calib1[26] = {};
|
||||
uint8_t calib2[7] = {};
|
||||
if (!readI2CBuffer(wire, addr, 0x88, calib1, sizeof(calib1)) ||
|
||||
!readI2CBuffer(wire, addr, 0xE1, calib2, sizeof(calib2))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BME280Cal.dig_T1 = u16le(&calib1[0]);
|
||||
BME280Cal.dig_T2 = s16le(&calib1[2]);
|
||||
BME280Cal.dig_T3 = s16le(&calib1[4]);
|
||||
BME280Cal.dig_P1 = u16le(&calib1[6]);
|
||||
BME280Cal.dig_P2 = s16le(&calib1[8]);
|
||||
BME280Cal.dig_P3 = s16le(&calib1[10]);
|
||||
BME280Cal.dig_P4 = s16le(&calib1[12]);
|
||||
BME280Cal.dig_P5 = s16le(&calib1[14]);
|
||||
BME280Cal.dig_P6 = s16le(&calib1[16]);
|
||||
BME280Cal.dig_P7 = s16le(&calib1[18]);
|
||||
BME280Cal.dig_P8 = s16le(&calib1[20]);
|
||||
BME280Cal.dig_P9 = s16le(&calib1[22]);
|
||||
BME280Cal.dig_H1 = calib1[25];
|
||||
BME280Cal.dig_H2 = s16le(&calib2[0]);
|
||||
BME280Cal.dig_H3 = calib2[2];
|
||||
BME280Cal.dig_H4 = ((int16_t)((int8_t)calib2[3]) << 4) | (calib2[4] & 0x0F);
|
||||
BME280Cal.dig_H5 = ((int16_t)((int8_t)calib2[5]) << 4) | (calib2[4] >> 4);
|
||||
BME280Cal.dig_H6 = (int8_t)calib2[6];
|
||||
return BME280Cal.dig_T1 != 0 && BME280Cal.dig_P1 != 0;
|
||||
}
|
||||
|
||||
static float compensate_bme280_temperature(int32_t adc_T) {
|
||||
int32_t var1 = ((((adc_T >> 3) - ((int32_t)BME280Cal.dig_T1 << 1))) * ((int32_t)BME280Cal.dig_T2)) >> 11;
|
||||
int32_t var2 = (((((adc_T >> 4) - ((int32_t)BME280Cal.dig_T1)) * ((adc_T >> 4) - ((int32_t)BME280Cal.dig_T1))) >> 12) *
|
||||
((int32_t)BME280Cal.dig_T3)) >> 14;
|
||||
BME280TFine = var1 + var2;
|
||||
return ((BME280TFine * 5 + 128) >> 8) / 100.0f;
|
||||
}
|
||||
|
||||
static float compensate_bme280_pressure(int32_t adc_P) {
|
||||
int64_t var1 = ((int64_t)BME280TFine) - 128000;
|
||||
int64_t var2 = var1 * var1 * (int64_t)BME280Cal.dig_P6;
|
||||
var2 = var2 + ((var1 * (int64_t)BME280Cal.dig_P5) << 17);
|
||||
var2 = var2 + (((int64_t)BME280Cal.dig_P4) << 35);
|
||||
var1 = ((var1 * var1 * (int64_t)BME280Cal.dig_P3) >> 8) + ((var1 * (int64_t)BME280Cal.dig_P2) << 12);
|
||||
var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)BME280Cal.dig_P1) >> 33;
|
||||
if (var1 == 0) {
|
||||
return NAN;
|
||||
}
|
||||
int64_t p = 1048576 - adc_P;
|
||||
p = (((p << 31) - var2) * 3125) / var1;
|
||||
var1 = (((int64_t)BME280Cal.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
|
||||
var2 = (((int64_t)BME280Cal.dig_P8) * p) >> 19;
|
||||
p = ((p + var1 + var2) >> 8) + (((int64_t)BME280Cal.dig_P7) << 4);
|
||||
return (float)p / 256.0f;
|
||||
}
|
||||
|
||||
static float compensate_bme280_humidity(int32_t adc_H) {
|
||||
int32_t v_x1_u32r = BME280TFine - 76800;
|
||||
v_x1_u32r = (((((adc_H << 14) - (((int32_t)BME280Cal.dig_H4) << 20) -
|
||||
(((int32_t)BME280Cal.dig_H5) * v_x1_u32r)) + 16384) >> 15) *
|
||||
(((((((v_x1_u32r * ((int32_t)BME280Cal.dig_H6)) >> 10) *
|
||||
(((v_x1_u32r * ((int32_t)BME280Cal.dig_H3)) >> 11) + 32768)) >> 10) + 2097152) *
|
||||
((int32_t)BME280Cal.dig_H2) + 8192) >> 14));
|
||||
v_x1_u32r = v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)BME280Cal.dig_H1)) >> 4);
|
||||
v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r;
|
||||
v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r;
|
||||
return (v_x1_u32r >> 12) / 1024.0f;
|
||||
}
|
||||
|
||||
static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
|
||||
if (!BME280.begin(addr, wire)) return 0;
|
||||
uint8_t chip_id = 0;
|
||||
const bool chip_id_ok = readI2CRegister8(wire, addr, 0xD0, &chip_id);
|
||||
if (!chip_id_ok || chip_id != 0x60 || !read_bme280_calibration(wire, addr)) {
|
||||
return 0;
|
||||
}
|
||||
BME280Wire = wire;
|
||||
BME280Address = addr;
|
||||
writeI2CRegister8(wire, addr, 0xF2, 0x01); // humidity oversampling x1
|
||||
writeI2CRegister8(wire, addr, 0xF5, 0x00); // filter off, standby 0.5 ms
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
if (BME280Wire == nullptr || BME280Address == 0) {
|
||||
return;
|
||||
}
|
||||
if (!writeI2CRegister8(BME280Wire, BME280Address, 0xF2, 0x01) ||
|
||||
!writeI2CRegister8(BME280Wire, BME280Address, 0xF4, 0x25)) { // temp/pressure x1, forced mode
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
uint8_t data[8] = {};
|
||||
if (!readI2CBuffer(BME280Wire, BME280Address, 0xF7, data, sizeof(data))) {
|
||||
return;
|
||||
}
|
||||
const int32_t adc_P = ((int32_t)data[0] << 12) | ((int32_t)data[1] << 4) | (data[2] >> 4);
|
||||
const int32_t adc_T = ((int32_t)data[3] << 12) | ((int32_t)data[4] << 4) | (data[5] >> 4);
|
||||
const int32_t adc_H = ((int32_t)data[6] << 8) | data[7];
|
||||
const float temperature = compensate_bme280_temperature(adc_T);
|
||||
const float pressure_pa = compensate_bme280_pressure(adc_P);
|
||||
const float humidity = compensate_bme280_humidity(adc_H);
|
||||
lpp.addTemperature(ch, temperature);
|
||||
lpp.addRelativeHumidity(ch, humidity);
|
||||
lpp.addBarometricPressure(ch, pressure_pa / 100.0f);
|
||||
lpp.addAltitude(ch, 44330.0f * (1.0f - powf((pressure_pa / 100.0f) / (float)TELEM_BME280_SEALEVELPRESSURE_HPA, 0.1903f)));
|
||||
}
|
||||
#else
|
||||
static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
|
||||
if (!BME280.begin(addr, wire)) {
|
||||
return 0;
|
||||
}
|
||||
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
|
||||
Adafruit_BME280::SAMPLING_X1,
|
||||
Adafruit_BME280::SAMPLING_X1,
|
||||
@@ -288,6 +487,7 @@ static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
static uint8_t init_bmp280(TwoWire*, uint8_t addr) {
|
||||
@@ -630,26 +830,36 @@ bool EnvironmentSensorManager::begin() {
|
||||
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
|
||||
#elif defined(ESP32) && defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
|
||||
Wire.setClock(100000);
|
||||
MESH_DEBUG_PRINTLN("I2C using board pins SDA: %d SCL: %d", PIN_BOARD_SDA, PIN_BOARD_SCL);
|
||||
#endif
|
||||
|
||||
// Scan the I2C bus before touching any sensor library.
|
||||
bool detected[128] = {};
|
||||
scanI2CBus(TELEM_WIRE, detected);
|
||||
|
||||
// Walk the sensor table and initialize only detected devices.
|
||||
_active_sensor_count = 0;
|
||||
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
|
||||
const SensorDef& def = SENSOR_TABLE[i];
|
||||
if (!detected[def.address]) {
|
||||
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
|
||||
uint8_t address = def.address;
|
||||
const uint8_t board_detected_address = boardDetectedI2CSensorAddress(def.name);
|
||||
const bool board_detected = board_detected_address != 0;
|
||||
if (board_detected) {
|
||||
address = board_detected_address;
|
||||
if (address == def.address) {
|
||||
MESH_DEBUG_PRINTLN("%s already detected by board scan at I2C address %02X", def.name, address);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("%s already detected by board scan at I2C address %02X (configured %02X)", def.name, address, def.address);
|
||||
}
|
||||
}
|
||||
if (!board_detected && !probeI2CAddress(TELEM_WIRE, address)) {
|
||||
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, address);
|
||||
continue;
|
||||
}
|
||||
uint8_t n = def.init(TELEM_WIRE, def.address);
|
||||
uint8_t n = def.init(TELEM_WIRE, address);
|
||||
if (n == 0) {
|
||||
MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, def.address);
|
||||
MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, address);
|
||||
continue;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
|
||||
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, address);
|
||||
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
|
||||
_active_sensors[_active_sensor_count++] = { def.query, sub };
|
||||
}
|
||||
|
||||
@@ -514,6 +514,9 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
:root[data-theme="dark"] .metric { background:rgba(0,0,0,.16); }
|
||||
.metric-label { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; }
|
||||
.metric-value { margin-top:4px; font-size:16px; font-weight:700; color:var(--text); }
|
||||
.metric-value.with-action { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
|
||||
.metric-text-action { width:auto; min-height:0; padding:0; border:none; background:transparent; color:var(--status-red); font:inherit; font-size:12px; font-weight:700; line-height:1.2; text-decoration:underline; }
|
||||
.metric-text-action:hover { background:transparent; color:var(--status-red); text-decoration:none; }
|
||||
.events-table-wrap { overflow-x:auto; border:1px solid var(--border); border-radius:12px; }
|
||||
.events-table { width:100%; border-collapse:collapse; }
|
||||
.events-table th, .events-table td { padding:10px 12px; text-align:left; font-size:13px; }
|
||||
@@ -1858,10 +1861,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
<div class="meter"><div class="meter-fill${tone ? " " + tone : ""}" style="width:${pct}%;${invertFill ? "margin-left:auto;" : ""}"></div></div>
|
||||
</div>`;
|
||||
}
|
||||
function renderMetric(label, value) {
|
||||
function renderMetric(label, value, actionHtml = "") {
|
||||
const valueClass = actionHtml ? "metric-value with-action" : "metric-value";
|
||||
return `<div class="metric">
|
||||
<div class="metric-label">${escapeHtml(label)}</div>
|
||||
<div class="metric-value">${escapeHtml(value)}</div>
|
||||
<div class="${valueClass}"><span>${escapeHtml(value)}</span>${actionHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
function hasMetricValue(value) {
|
||||
@@ -1874,7 +1878,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
if (!items.length) return "";
|
||||
const classes = ["metric-grid"];
|
||||
if (gridClass) classes.push(gridClass);
|
||||
return `<div class="${classes.join(" ")}">${items.map((item) => renderMetric(item.label, item.value)).join("")}</div>`;
|
||||
return `<div class="${classes.join(" ")}">${items.map((item) => renderMetric(item.label, item.value, item.actionHtml || "")).join("")}</div>`;
|
||||
}
|
||||
function renderMissingCard(title, message) {
|
||||
return `<section class="hud-card">
|
||||
@@ -2080,7 +2084,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
metrics.push(
|
||||
{ label:"Card", value:archive.type || "--" },
|
||||
{ label:"Archive Total", value:formatArchiveGigabytes(archiveTotal) },
|
||||
{ label:"Archive Used", value:formatArchiveUsed(archiveUsed) },
|
||||
{ label:"Archive Used", value:formatArchiveUsed(archiveUsed), actionHtml:'<button type="button" class="metric-text-action" data-action="purge-sd">Purge SD</button>' },
|
||||
{ label:"Archive Free", value:archiveTotal > 0 ? (formatArchiveGigabytes(archiveFree) + " (" + archiveFreePct + "%)") : "--" }
|
||||
);
|
||||
}
|
||||
@@ -3139,6 +3143,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", async (event) => {
|
||||
const target = event.target;
|
||||
if (!target || !target.matches || !target.matches('[data-action="purge-sd"]')) return;
|
||||
if (confirm("Purge the SD card archive now? This deletes all files on the mounted SD card.")) {
|
||||
await runCommand("purge sd");
|
||||
if (isStatsPage) {
|
||||
await loadStatsPage(nextPageLoadGeneration());
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById("refreshPageBtn").onclick = async () => {
|
||||
const generation = nextPageLoadGeneration();
|
||||
if (isStatsPage) {
|
||||
@@ -3317,7 +3331,7 @@ bool WebPanelServer::start() {
|
||||
WEB_PANEL_LOG("redirect server start failed rc=0x%x", static_cast<unsigned>(rc));
|
||||
}
|
||||
|
||||
WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
|
||||
WEB_PANEL_LOG("server started t=%lu on https://%s/", static_cast<unsigned long>(millis()), WiFi.localIP().toString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user