mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-08-28 05:24:32 +00:00
Merge branch 'ratspeak:main' into ls-add-battery-bar
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
[](#install)
|
||||
[](LICENSE)
|
||||
[](https://github.com/ratspeak/rsDeck/releases)
|
||||
[](https://github.com/ratspeak/rsDeck/releases)
|
||||
|
||||
[Ratspeak](https://github.com/ratspeak/Ratspeak) |
|
||||
[Docs](https://ratspeak.org/docs.html) |
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
lib_deps =
|
||||
https://github.com/ratspeak/microReticulum.git#3ddf3c362bdf05d1188ba2245c2ea06cb6514d40
|
||||
https://github.com/ratspeak/microReticulum.git#83b88146f4a49bb46ea6e0c22c560257c33815d7
|
||||
bblanchon/ArduinoJson@^7.4.2
|
||||
lovyan03/LovyanGFX@~1.1.16
|
||||
lvgl/lvgl@~8.3.4
|
||||
|
||||
+3
-3
@@ -6,8 +6,8 @@
|
||||
|
||||
#define RSDECK_VERSION_MAJOR 2
|
||||
#define RSDECK_VERSION_MINOR 0
|
||||
#define RSDECK_VERSION_PATCH 0
|
||||
#define RSDECK_VERSION_STRING "2.0.0"
|
||||
#define RSDECK_VERSION_PATCH 1
|
||||
#define RSDECK_VERSION_STRING "2.0.1"
|
||||
|
||||
// --- Feature Flags ---
|
||||
#define HAS_DISPLAY true
|
||||
@@ -68,7 +68,7 @@
|
||||
#define RSDECK_MAX_MESSAGES_PER_CONV 100
|
||||
#define FLASH_MSG_CACHE_LIMIT 20
|
||||
#define RSDECK_MAX_OUTQUEUE 20
|
||||
#define RSDECK_LXMF_SINGLE_FRAME_MAX 254 // T-Deck-safe payload cap until resource transfers are fixed
|
||||
#define RSDECK_RNODE_SINGLE_FRAME_RAW_MAX 254 // Raw Reticulum bytes per RNode LoRa RF frame
|
||||
#define PATH_PERSIST_INTERVAL_MS 60000
|
||||
|
||||
// --- Power Management ---
|
||||
|
||||
+4
-2
@@ -711,7 +711,7 @@ static constexpr uint8_t LITE_TRANSPORT_ID[16] = {
|
||||
'r', 's', 'l', 'i', 't', 'e', '-', 'h',
|
||||
'e', 'l', 't', 'e', 'c', '-', 'v', '3'
|
||||
};
|
||||
static constexpr size_t RNODE_DIAG_SINGLE_MTU = 254;
|
||||
static constexpr size_t RNODE_DIAG_SINGLE_MTU = RSDECK_RNODE_SINGLE_FRAME_RAW_MAX;
|
||||
static RNS::Bytes diagnosticLiteLinkId;
|
||||
|
||||
static bool sendDiagnosticRawReticulum(const RNS::Bytes& raw, const char* label) {
|
||||
@@ -1878,7 +1878,9 @@ void loop() {
|
||||
handleSerialCommands();
|
||||
|
||||
// 1. Input polling
|
||||
bool screenWasOn = powerMgr.isScreenOn();
|
||||
inputManager.update();
|
||||
bool wakeOnlyInput = !screenWasOn && inputManager.hadStrongActivity();
|
||||
if (inputManager.hadStrongActivity()) {
|
||||
powerMgr.activity(); // Keyboard/touch: wake from any state
|
||||
} else if (inputManager.hadActivity()) {
|
||||
@@ -1893,7 +1895,7 @@ void loop() {
|
||||
}
|
||||
|
||||
// 3. Key event dispatch
|
||||
if (inputManager.hasKeyEvent()) {
|
||||
if (inputManager.hasKeyEvent() && !wakeOnlyInput) {
|
||||
const KeyEvent& evt = inputManager.getKeyEvent();
|
||||
|
||||
// Help overlay intercepts all keys when visible
|
||||
|
||||
@@ -152,6 +152,7 @@ void AnnounceManager::received_announce(
|
||||
auto& node = _nodes[it->second];
|
||||
if (node.lastSeen != 0 && now >= node.lastSeen &&
|
||||
now - node.lastSeen < ANNOUNCE_MIN_INTERVAL_MS) return;
|
||||
bool identityChanged = !idHex.empty() && node.identityHex != idHex;
|
||||
// Saved contacts own their local alias. Incoming announce app_data is
|
||||
// still cached below, but must not reset the user-chosen contact name.
|
||||
if (!name.empty() && !node.saved) node.name = name;
|
||||
@@ -167,8 +168,15 @@ void AnnounceManager::received_announce(
|
||||
if (nc == _nameCache.end() || nc->second != name) {
|
||||
_nameCache[destHex] = name;
|
||||
_nameCacheDirty = true;
|
||||
saveNameCache();
|
||||
_nameCacheDirty = false;
|
||||
}
|
||||
}
|
||||
if (!idHex.empty()) {
|
||||
persistKnownDestinationsAfterAnnounce(
|
||||
identityChanged ? "identity update" : "repeat announce",
|
||||
identityChanged);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,6 +200,8 @@ void AnnounceManager::received_announce(
|
||||
}
|
||||
}
|
||||
}
|
||||
saveNameCache();
|
||||
_nameCacheDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +252,9 @@ void AnnounceManager::received_announce(
|
||||
if (_loraIf) { node.rssi = _loraIf->lastRxRssi(); node.snr = _loraIf->lastRxSnr(); }
|
||||
_hashIndex[key] = (int)_nodes.size();
|
||||
_nodes.push_back(node);
|
||||
if (!idHex.empty()) {
|
||||
persistKnownDestinationsAfterAnnounce("new peer", true);
|
||||
}
|
||||
}
|
||||
|
||||
void AnnounceManager::loop() {
|
||||
@@ -258,6 +271,19 @@ void AnnounceManager::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void AnnounceManager::persistKnownDestinationsAfterAnnounce(const char* reason, bool force) {
|
||||
unsigned long now = millis();
|
||||
if (!force && _lastKnownDestinationsPersist != 0 &&
|
||||
now - _lastKnownDestinationsPersist < KNOWN_DESTINATION_PERSIST_MIN_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastKnownDestinationsPersist = now;
|
||||
RNS::Identity::persist_data();
|
||||
Serial.printf("[ANNOUNCE] Known destinations persisted after %s\n",
|
||||
reason ? reason : "announce");
|
||||
}
|
||||
|
||||
int AnnounceManager::nodesOnlineSince(unsigned long maxAgeMs) const {
|
||||
unsigned long now = millis();
|
||||
int count = 0;
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
private:
|
||||
void saveContact(const DiscoveredNode& node);
|
||||
void removeContact(const std::string& hexHash);
|
||||
void persistKnownDestinationsAfterAnnounce(const char* reason, bool force);
|
||||
|
||||
std::vector<DiscoveredNode> _nodes;
|
||||
SDStore* _sd = nullptr;
|
||||
@@ -77,7 +78,9 @@ private:
|
||||
static constexpr int MAX_NODES = 100;
|
||||
static constexpr int MAX_NAME_CACHE = 300;
|
||||
static constexpr unsigned long CONTACT_SAVE_INTERVAL_MS = 30000;
|
||||
static constexpr unsigned long KNOWN_DESTINATION_PERSIST_MIN_INTERVAL_MS = 5000;
|
||||
static constexpr unsigned long ANNOUNCE_MIN_INTERVAL_MS = 200; // Rate-limit announce processing
|
||||
unsigned long _lastKnownDestinationsPersist = 0;
|
||||
|
||||
std::unordered_map<std::string, int> _hashIndex; // raw hash bytes → _nodes index
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <Transport.h>
|
||||
#include <time.h>
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
|
||||
LXMFManager* LXMFManager::_instance = nullptr;
|
||||
std::map<std::string, LXMFManager::PendingProof> LXMFManager::_pendingProofs;
|
||||
@@ -71,7 +72,7 @@ void LXMFManager::loop() {
|
||||
|
||||
// Keep unresolved peers from churning the UI loop or LoRa airtime.
|
||||
// The first attempt is immediate; later path/identity retries happen
|
||||
// every 10s and are capped in sendDirect().
|
||||
// every 10s and are capped in attemptOutboundDelivery().
|
||||
if (msg.retries > 0 && (millis() - msg.lastRetryMs) < LXMF_DISCOVERY_RETRY_INTERVAL_MS) {
|
||||
++it;
|
||||
continue;
|
||||
@@ -79,7 +80,7 @@ void LXMFManager::loop() {
|
||||
|
||||
msg.lastRetryMs = millis();
|
||||
|
||||
if (sendDirect(msg)) {
|
||||
if (attemptOutboundDelivery(msg)) {
|
||||
processed++;
|
||||
Serial.printf("[LXMF] Queue drain: status=%s dest=%s\n",
|
||||
msg.statusStr(), msg.destHash.toHex().substr(0, 8).c_str());
|
||||
@@ -98,7 +99,7 @@ void LXMFManager::loop() {
|
||||
clearDeliveryPreference(msg);
|
||||
it = _outQueue.erase(it);
|
||||
} else {
|
||||
// sendDirect returned false — message stays in queue, try next
|
||||
// attemptOutboundDelivery returned false — message stays in queue, try next
|
||||
++it;
|
||||
}
|
||||
}
|
||||
@@ -131,13 +132,6 @@ bool LXMFManager::sendMessage(const RNS::Bytes& destHash, const std::string& con
|
||||
Serial.println("[LXMF] Message pack failed");
|
||||
return false;
|
||||
}
|
||||
if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) {
|
||||
msg.status = LXMFStatus::FAILED;
|
||||
if (_store) _store->saveMessage(msg);
|
||||
Serial.printf("[LXMF] Message too large for T-Deck safe path (%d > %d); resource transfer disabled\n",
|
||||
(int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX);
|
||||
return true;
|
||||
}
|
||||
if (preference == DeliveryPreference::Link) {
|
||||
_linkRequiredIds.insert(msg.messageId.toHex());
|
||||
Serial.printf("[LXMF] Message %s queued for link delivery\n",
|
||||
@@ -193,8 +187,8 @@ bool LXMFManager::ensureOutboundLink(const RNS::Destination& dest, const RNS::By
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LXMFManager::sendDirect(LXMFMessage& msg) {
|
||||
Serial.printf("[LXMF] sendDirect: dest=%s link=%s pending=%s\n",
|
||||
bool LXMFManager::attemptOutboundDelivery(LXMFMessage& msg) {
|
||||
Serial.printf("[LXMF] attemptOutboundDelivery: dest=%s link=%s pending=%s\n",
|
||||
msg.destHash.toHex().substr(0, 12).c_str(),
|
||||
_outLink ? (_outLink.status() == RNS::Type::Link::ACTIVE ? "ACTIVE" : "INACTIVE") : "NONE",
|
||||
_outLinkPending ? "yes" : "no");
|
||||
@@ -267,12 +261,6 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
|
||||
if (payload.empty()) { Serial.println("[LXMF] packFull returned empty!"); msg.status = LXMFStatus::FAILED; return true; }
|
||||
const std::string msgIdHex = msg.messageId.toHex();
|
||||
const bool requireLink = _linkRequiredIds.count(msgIdHex) > 0;
|
||||
if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) {
|
||||
Serial.printf("[LXMF] Refusing resource-sized message (%d bytes); T-Deck safe cap is %d\n",
|
||||
(int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX);
|
||||
msg.status = LXMFStatus::FAILED;
|
||||
return true;
|
||||
}
|
||||
|
||||
msg.status = LXMFStatus::SENDING;
|
||||
bool sent = false;
|
||||
@@ -334,24 +322,60 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use opportunistic only for packets that fit in a single LoRa frame (254 bytes).
|
||||
// Larger packets require split-frame over LoRa, which is unreliable — any single
|
||||
// frame loss (CRC error, collision, half-duplex timing) kills the entire transfer
|
||||
// with no recovery. Link-based delivery handles retransmission at the protocol level.
|
||||
if (payloadBytes.size() <= RSDECK_LXMF_SINGLE_FRAME_MAX) {
|
||||
// Fits in single LoRa frame — send opportunistic
|
||||
Serial.printf("[LXMF] sending opportunistic: %d bytes to %s\n",
|
||||
(int)payloadBytes.size(), outDest.hash().toHex().substr(0, 12).c_str());
|
||||
RNS::Packet packet(outDest, payloadBytes);
|
||||
RNS::PacketReceipt receipt = packet.send();
|
||||
if (receipt) { sent = true; registerProofTracking(receipt, msg); }
|
||||
// Use opportunistic only when the LXMF payload fits in a normal
|
||||
// Reticulum packet and, on LoRa, the final raw packet fits in one
|
||||
// RNode RF frame. The 254-byte RNode limit applies after encryption.
|
||||
const bool opportunisticPayloadFits = payloadBytes.size() <= RNS::Type::Reticulum::MDU;
|
||||
bool packetPacked = false;
|
||||
size_t rawLen = 0;
|
||||
size_t estimatedLoRaRawLen = 0;
|
||||
const bool loraPath = _rns && _rns->isLoRaNextHop(msg.destHash);
|
||||
const uint8_t hops = RNS::Transport::hops_to(msg.destHash);
|
||||
|
||||
if (!opportunisticPayloadFits) {
|
||||
Serial.printf("[LXMF] Opportunistic payload exceeds MDU: payload=%d mdu=%u\n",
|
||||
(int)payloadBytes.size(), (unsigned)RNS::Type::Reticulum::MDU);
|
||||
} else {
|
||||
// Too large for single frame — need link + resource transfer
|
||||
Serial.printf("[LXMF] Message needs link delivery (%d bytes > %d single-frame), retry %d\n",
|
||||
(int)payloadBytes.size(), RSDECK_LXMF_SINGLE_FRAME_MAX, msg.retries);
|
||||
RNS::Packet packet(outDest, payloadBytes);
|
||||
const bool transportWrapsHeader2 = loraPath && hops > 1
|
||||
&& packet.header_type() == RNS::Type::Packet::HEADER_1;
|
||||
|
||||
try {
|
||||
packet.pack();
|
||||
packetPacked = true;
|
||||
rawLen = packet.raw().size();
|
||||
estimatedLoRaRawLen = rawLen
|
||||
+ (transportWrapsHeader2 ? RNS::Type::Reticulum::DESTINATION_LENGTH : 0);
|
||||
} catch (const std::exception& e) {
|
||||
Serial.printf("[LXMF] Opportunistic pack failed (%s); trying link delivery\n", e.what());
|
||||
}
|
||||
|
||||
const bool singleFrameSafe = packetPacked && (!loraPath
|
||||
|| estimatedLoRaRawLen <= RSDECK_RNODE_SINGLE_FRAME_RAW_MAX);
|
||||
|
||||
if (singleFrameSafe) {
|
||||
Serial.printf("[LXMF] sending opportunistic: payload=%d raw=%u lora_raw=%u lora=%s hops=%u to %s\n",
|
||||
(int)payloadBytes.size(), (unsigned)rawLen,
|
||||
(unsigned)estimatedLoRaRawLen,
|
||||
loraPath ? "yes" : "no", (unsigned)hops,
|
||||
outDest.hash().toHex().substr(0, 12).c_str());
|
||||
RNS::PacketReceipt receipt = packet.send();
|
||||
if (receipt) { sent = true; registerProofTracking(receipt, msg); }
|
||||
}
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
// Too large for single-frame LoRa, or too large for an opportunistic
|
||||
// Reticulum packet. Use link/resource delivery so packet loss is
|
||||
// recoverable above the physical RNode split-frame layer.
|
||||
Serial.printf("[LXMF] Message needs link delivery: payload=%d raw=%u lora_raw=%u limit=%u lora=%s hops=%u retry=%d\n",
|
||||
(int)payloadBytes.size(), (unsigned)rawLen,
|
||||
(unsigned)estimatedLoRaRawLen,
|
||||
(unsigned)RSDECK_RNODE_SINGLE_FRAME_RAW_MAX,
|
||||
loraPath ? "yes" : "no", (unsigned)hops, msg.retries);
|
||||
if (msg.retries % 3 == 0 && (!_outLink || _outLinkDestHash != msg.destHash
|
||||
|| _outLink.status() != RNS::Type::Link::ACTIVE)) {
|
||||
ensureOutboundLink(outDest, msg.destHash, "resource transfer");
|
||||
ensureOutboundLink(outDest, msg.destHash, loraPath ? "single-frame LoRa overflow" : "resource transfer");
|
||||
}
|
||||
msg.retries++;
|
||||
if (msg.retries >= 30) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
const ConversationSummary* getConversationSummary(const std::string& peerHex) const;
|
||||
|
||||
private:
|
||||
bool sendDirect(LXMFMessage& msg);
|
||||
bool attemptOutboundDelivery(LXMFMessage& msg);
|
||||
bool ensureOutboundLink(const RNS::Destination& dest, const RNS::Bytes& destHash, const char* reason);
|
||||
void clearDeliveryPreference(const LXMFMessage& msg);
|
||||
void processIncoming(const uint8_t* data, size_t len, const RNS::Bytes& destHash);
|
||||
|
||||
@@ -210,7 +210,7 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash, bool loraEnabled)
|
||||
if (loraEnabled) {
|
||||
_loraImpl = new LoRaInterface(radio, "LoRa");
|
||||
_loraIface = _loraImpl;
|
||||
_loraIface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
||||
_loraIface.mode(RNS::Type::Interface::MODE_ROAMING);
|
||||
RNS::Transport::register_interface(_loraIface);
|
||||
if (!_loraImpl->start()) {
|
||||
Serial.println("[RNS] WARNING: LoRa interface failed to start");
|
||||
@@ -274,6 +274,12 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash, bool loraEnabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReticulumManager::isLoRaNextHop(const RNS::Bytes& destHash) const {
|
||||
if (!_loraImpl) return false;
|
||||
RNS::Interface nextHop = RNS::Transport::next_hop_interface(destHash);
|
||||
return nextHop && nextHop.get() == _loraImpl;
|
||||
}
|
||||
|
||||
bool ReticulumManager::loadOrCreateIdentity() {
|
||||
// Tier 1: Flash (LittleFS)
|
||||
if (_flash->exists(PATH_IDENTITY)) {
|
||||
|
||||
@@ -56,6 +56,7 @@ public:
|
||||
|
||||
RNS::Destination& destination() { return _destination; }
|
||||
LoRaInterface* loraInterface() { return _loraImpl; }
|
||||
bool isLoRaNextHop(const RNS::Bytes& destHash) const;
|
||||
|
||||
private:
|
||||
bool loadOrCreateIdentity();
|
||||
|
||||
@@ -714,7 +714,7 @@ bool LvMessageView::handleLongPress() {
|
||||
void LvMessageView::showSendModeMenu() {
|
||||
if (_inputText.empty()) return;
|
||||
hideSendModeMenu();
|
||||
_sendMenuIdx = 1;
|
||||
_sendMenuIdx = 0;
|
||||
|
||||
_sendOverlay = lv_obj_create(lv_layer_top());
|
||||
lv_obj_set_size(_sendOverlay, 244, 118);
|
||||
|
||||
@@ -176,9 +176,9 @@ bool LvSettingsScreen::settingNeedsReboot(const SettingItem& item) const {
|
||||
const auto& s = _cfg->settings();
|
||||
if (labelEq(item.label, "WiFi Mode")) return s.wifiMode != _rebootSnap.wifiMode;
|
||||
if (labelEq(item.label, "LoRa Radio")) return loraSettingsChanged();
|
||||
if (labelEq(item.label, "Active WiFi")) return s.wifiSTASelected != _rebootSnap.wifiSTASelected;
|
||||
if (labelEq(item.label, "WiFi Profile")) return s.wifiSTASelected != _rebootSnap.wifiSTASelected;
|
||||
if (isWiFiSSIDLabel(item.label) || isWiFiPasswordLabel(item.label)) return interfaceSettingsChanged();
|
||||
if (labelEq(item.label, "WiFi Scan") || labelEq(item.label, "Forget WiFi")) return interfaceSettingsChanged();
|
||||
if (labelEq(item.label, "Scan Networks") || labelEq(item.label, "Forget Network")) return interfaceSettingsChanged();
|
||||
if (labelEq(item.label, "TCP Relay") || labelEq(item.label, "Relay Host") ||
|
||||
labelEq(item.label, "Relay Port")) return tcpSettingsChanged();
|
||||
if (labelEq(item.label, "LAN Discovery")) return s.autoIfaceEnabled != _rebootSnap.autoIfaceEnabled;
|
||||
@@ -191,7 +191,7 @@ bool LvSettingsScreen::categoryNeedsReboot(int catIdx) const {
|
||||
if (labelEq(_categories[catIdx].name, "LoRa")) {
|
||||
return loraSettingsChanged();
|
||||
}
|
||||
if (labelEq(_categories[catIdx].name, "Interfaces")) {
|
||||
if (labelEq(_categories[catIdx].name, "Network")) {
|
||||
return interfaceSettingsChanged() || tcpSettingsChanged();
|
||||
}
|
||||
if (labelEq(_categories[catIdx].name, "Storage & Maintenance")) {
|
||||
@@ -774,7 +774,7 @@ void LvSettingsScreen::buildItems() {
|
||||
return label;
|
||||
}});
|
||||
|
||||
// Interfaces
|
||||
// Network
|
||||
int netStart = idx;
|
||||
_items.push_back({"WiFi Mode", SettingType::ENUM_CHOICE,
|
||||
[&s]() { return (int)s.wifiMode; },
|
||||
@@ -784,7 +784,19 @@ void LvSettingsScreen::buildItems() {
|
||||
},
|
||||
nullptr, 0, 2, 1, {"Off", "Hotspot", "Client"}});
|
||||
idx++;
|
||||
_items.push_back({"Active WiFi", SettingType::INTEGER,
|
||||
{
|
||||
SettingItem scanItem;
|
||||
scanItem.label = "Scan Networks";
|
||||
scanItem.type = SettingType::ACTION;
|
||||
scanItem.formatter = [](int) { return String("[Enter]"); };
|
||||
scanItem.action = [this, &s]() {
|
||||
_wifiTargetSlot = selectedWiFiSlot(s);
|
||||
showWifiPicker();
|
||||
};
|
||||
_items.push_back(scanItem);
|
||||
idx++;
|
||||
}
|
||||
_items.push_back({"WiFi Profile", SettingType::INTEGER,
|
||||
[&s]() { return (int)selectedWiFiSlot(s) + 1; },
|
||||
[&s](int v) { s.wifiSTASelected = (uint8_t)constrain(v - 1, 0, (int)WIFI_STA_MAX_NETWORKS - 1); },
|
||||
[&s](int v) { return wifiProfileValue(s, constrain(v - 1, 0, (int)WIFI_STA_MAX_NETWORKS - 1)); },
|
||||
@@ -827,21 +839,9 @@ void LvSettingsScreen::buildItems() {
|
||||
_items.push_back(passItem);
|
||||
idx++;
|
||||
}
|
||||
{
|
||||
SettingItem scanItem;
|
||||
scanItem.label = "WiFi Scan";
|
||||
scanItem.type = SettingType::ACTION;
|
||||
scanItem.formatter = [](int) { return String("[Enter]"); };
|
||||
scanItem.action = [this, &s]() {
|
||||
_wifiTargetSlot = selectedWiFiSlot(s);
|
||||
showWifiPicker();
|
||||
};
|
||||
_items.push_back(scanItem);
|
||||
idx++;
|
||||
}
|
||||
{
|
||||
SettingItem forgetItem;
|
||||
forgetItem.label = "Forget WiFi";
|
||||
forgetItem.label = "Forget Network";
|
||||
forgetItem.type = SettingType::ACTION;
|
||||
forgetItem.formatter = [&s](int) {
|
||||
String ssid = selectedWiFiSSID(s);
|
||||
@@ -921,7 +921,7 @@ void LvSettingsScreen::buildItems() {
|
||||
[&s](int v) { s.autoIfaceEnabled = (v != 0); },
|
||||
[](int v) { return String(onOff(v != 0)); }});
|
||||
idx++;
|
||||
_categories.push_back({"Interfaces", netStart, idx - netStart,
|
||||
_categories.push_back({"Network", netStart, idx - netStart,
|
||||
[this, &s]() {
|
||||
if (interfaceSettingsChanged() || tcpSettingsChanged()) return String("Saved - reboot to apply");
|
||||
String summary = wifiModeLabel(s.wifiMode);
|
||||
|
||||
Reference in New Issue
Block a user