crosspoint-reader/src/screens/WifiScreen.cpp

583 lines
17 KiB
C++
Raw Normal View History

2025-12-15 20:56:09 -05:00
#include "WifiScreen.h"
#include <GfxRenderer.h>
#include <WiFi.h>
2025-12-15 21:23:21 -05:00
#include "CrossPointWebServer.h"
2025-12-15 22:02:03 -05:00
#include "WifiCredentialStore.h"
2025-12-15 20:56:09 -05:00
#include "config.h"
void WifiScreen::taskTrampoline(void* param) {
auto* self = static_cast<WifiScreen*>(param);
self->displayTaskLoop();
}
void WifiScreen::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
2025-12-15 22:02:03 -05:00
// Load saved WiFi credentials
WIFI_STORE.loadFromFile();
2025-12-15 20:56:09 -05:00
// Reset state
selectedNetworkIndex = 0;
networks.clear();
state = WifiScreenState::SCANNING;
selectedSSID.clear();
connectedIP.clear();
connectionError.clear();
2025-12-15 22:02:03 -05:00
enteredPassword.clear();
usedSavedPassword = false;
savePromptSelection = 0;
2025-12-15 20:56:09 -05:00
keyboard.reset();
// Trigger first update to show scanning message
updateRequired = true;
xTaskCreate(&WifiScreen::taskTrampoline, "WifiScreenTask",
4096, // Stack size (larger for WiFi operations)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Start WiFi scan
startWifiScan();
}
void WifiScreen::onExit() {
// Stop any ongoing WiFi scan
WiFi.scanDelete();
2025-12-15 21:43:01 -05:00
// Stop the web server to free memory
crossPointWebServer.stop();
// Disconnect WiFi to free memory
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
2025-12-15 20:56:09 -05:00
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void WifiScreen::startWifiScan() {
state = WifiScreenState::SCANNING;
networks.clear();
updateRequired = true;
// Set WiFi mode to station
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
// Start async scan
WiFi.scanNetworks(true); // true = async scan
}
void WifiScreen::processWifiScanResults() {
int16_t scanResult = WiFi.scanComplete();
if (scanResult == WIFI_SCAN_RUNNING) {
// Scan still in progress
return;
}
if (scanResult == WIFI_SCAN_FAILED) {
state = WifiScreenState::NETWORK_LIST;
updateRequired = true;
return;
}
// Scan complete, process results
networks.clear();
for (int i = 0; i < scanResult; i++) {
WifiNetworkInfo network;
network.ssid = WiFi.SSID(i).c_str();
network.rssi = WiFi.RSSI(i);
network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
2025-12-15 22:02:03 -05:00
network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid);
2025-12-15 20:56:09 -05:00
// Skip hidden networks (empty SSID)
if (!network.ssid.empty()) {
networks.push_back(network);
}
}
// Sort by signal strength (strongest first)
std::sort(networks.begin(), networks.end(),
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
WiFi.scanDelete();
state = WifiScreenState::NETWORK_LIST;
selectedNetworkIndex = 0;
updateRequired = true;
}
void WifiScreen::selectNetwork(int index) {
if (index < 0 || index >= static_cast<int>(networks.size())) {
return;
}
const auto& network = networks[index];
selectedSSID = network.ssid;
selectedRequiresPassword = network.isEncrypted;
2025-12-15 22:02:03 -05:00
usedSavedPassword = false;
enteredPassword.clear();
// Check if we have saved credentials for this network
const auto* savedCred = WIFI_STORE.findCredential(selectedSSID);
if (savedCred && !savedCred->password.empty()) {
// Use saved password - connect directly
enteredPassword = savedCred->password;
usedSavedPassword = true;
attemptConnection();
return;
}
2025-12-15 20:56:09 -05:00
if (selectedRequiresPassword) {
// Show password entry
state = WifiScreenState::PASSWORD_ENTRY;
keyboard.reset(new OnScreenKeyboard(
renderer, inputManager,
"Enter WiFi Password",
"", // No initial text
64, // Max password length
false // Show password by default (hard keyboard to use)
));
updateRequired = true;
} else {
// Connect directly for open networks
attemptConnection();
}
}
void WifiScreen::attemptConnection() {
state = WifiScreenState::CONNECTING;
connectionStartTime = millis();
connectedIP.clear();
connectionError.clear();
updateRequired = true;
WiFi.mode(WIFI_STA);
2025-12-15 22:02:03 -05:00
// Get password from keyboard if we just entered it
if (keyboard && !usedSavedPassword) {
enteredPassword = keyboard->getText();
}
if (selectedRequiresPassword && !enteredPassword.empty()) {
WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str());
2025-12-15 20:56:09 -05:00
} else {
WiFi.begin(selectedSSID.c_str());
}
}
void WifiScreen::checkConnectionStatus() {
if (state != WifiScreenState::CONNECTING) {
return;
}
wl_status_t status = WiFi.status();
if (status == WL_CONNECTED) {
// Successfully connected
IPAddress ip = WiFi.localIP();
char ipStr[16];
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
connectedIP = ipStr;
2025-12-15 21:23:21 -05:00
// Start the web server
crossPointWebServer.begin();
2025-12-15 22:02:03 -05:00
// If we used a saved password, go directly to connected screen
// If we entered a new password, ask if user wants to save it
if (usedSavedPassword || enteredPassword.empty()) {
state = WifiScreenState::CONNECTED;
} else {
state = WifiScreenState::SAVE_PROMPT;
savePromptSelection = 0; // Default to "Yes"
}
updateRequired = true;
2025-12-15 20:56:09 -05:00
return;
}
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) {
connectionError = "Connection failed";
if (status == WL_NO_SSID_AVAIL) {
connectionError = "Network not found";
}
state = WifiScreenState::CONNECTION_FAILED;
updateRequired = true;
return;
}
// Check for timeout
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect();
connectionError = "Connection timeout";
state = WifiScreenState::CONNECTION_FAILED;
updateRequired = true;
return;
}
}
void WifiScreen::handleInput() {
// Check scan progress
if (state == WifiScreenState::SCANNING) {
processWifiScanResults();
return;
}
// Check connection progress
if (state == WifiScreenState::CONNECTING) {
checkConnectionStatus();
return;
}
// Handle password entry state
if (state == WifiScreenState::PASSWORD_ENTRY && keyboard) {
keyboard->handleInput();
if (keyboard->isComplete()) {
attemptConnection();
return;
}
if (keyboard->isCancelled()) {
state = WifiScreenState::NETWORK_LIST;
keyboard.reset();
updateRequired = true;
return;
}
updateRequired = true;
return;
}
2025-12-15 22:02:03 -05:00
// Handle save prompt state
if (state == WifiScreenState::SAVE_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) ||
inputManager.wasPressed(InputManager::BTN_UP)) {
if (savePromptSelection > 0) {
savePromptSelection--;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT) ||
inputManager.wasPressed(InputManager::BTN_DOWN)) {
if (savePromptSelection < 1) {
savePromptSelection++;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (savePromptSelection == 0) {
// User chose "Yes" - save the password
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
}
// Move to connected screen
state = WifiScreenState::CONNECTED;
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip saving, go to connected screen
state = WifiScreenState::CONNECTED;
updateRequired = true;
}
return;
}
2025-12-15 20:56:09 -05:00
// Handle connected/failed states
if (state == WifiScreenState::CONNECTED || state == WifiScreenState::CONNECTION_FAILED) {
if (inputManager.wasPressed(InputManager::BTN_BACK) ||
inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (state == WifiScreenState::CONNECTION_FAILED) {
// Go back to network list on failure
state = WifiScreenState::NETWORK_LIST;
updateRequired = true;
} else {
// Exit screen on success
onGoBack();
}
return;
}
}
// Handle network list state
if (state == WifiScreenState::NETWORK_LIST) {
// Check for Back button to exit
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
return;
}
// Check for Confirm button to select network or rescan
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (!networks.empty()) {
selectNetwork(selectedNetworkIndex);
} else {
startWifiScan();
}
return;
}
// Handle UP/DOWN navigation
if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
if (selectedNetworkIndex > 0) {
selectedNetworkIndex--;
updateRequired = true;
}
} else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
selectedNetworkIndex++;
updateRequired = true;
}
}
}
}
std::string WifiScreen::getSignalStrengthIndicator(int32_t rssi) const {
// Convert RSSI to signal bars representation
if (rssi >= -50) {
return "||||"; // Excellent
} else if (rssi >= -60) {
return "||| "; // Good
} else if (rssi >= -70) {
return "|| "; // Fair
} else if (rssi >= -80) {
return "| "; // Weak
}
return " "; // Very weak
}
void WifiScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void WifiScreen::render() const {
renderer.clearScreen();
switch (state) {
case WifiScreenState::SCANNING:
renderConnecting(); // Reuse connecting screen with different message
break;
case WifiScreenState::NETWORK_LIST:
renderNetworkList();
break;
case WifiScreenState::PASSWORD_ENTRY:
renderPasswordEntry();
break;
case WifiScreenState::CONNECTING:
renderConnecting();
break;
case WifiScreenState::CONNECTED:
renderConnected();
break;
2025-12-15 22:02:03 -05:00
case WifiScreenState::SAVE_PROMPT:
renderSavePrompt();
break;
2025-12-15 20:56:09 -05:00
case WifiScreenState::CONNECTION_FAILED:
renderConnectionFailed();
break;
}
renderer.displayBuffer();
}
void WifiScreen::renderNetworkList() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD);
if (networks.empty()) {
// No networks found or scan failed
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_FONT_ID, top, "No networks found", true, REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again", true, REGULAR);
} else {
// Calculate how many networks we can display
const int startY = 60;
const int lineHeight = 25;
const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight;
// Calculate scroll offset to keep selected item visible
int scrollOffset = 0;
if (selectedNetworkIndex >= maxVisibleNetworks) {
scrollOffset = selectedNetworkIndex - maxVisibleNetworks + 1;
}
// Draw networks
int displayIndex = 0;
for (size_t i = scrollOffset; i < networks.size() && displayIndex < maxVisibleNetworks; i++, displayIndex++) {
const int networkY = startY + displayIndex * lineHeight;
const auto& network = networks[i];
// Draw selection indicator
if (static_cast<int>(i) == selectedNetworkIndex) {
renderer.drawText(UI_FONT_ID, 5, networkY, ">");
}
// Draw network name (truncate if too long)
std::string displayName = network.ssid;
2025-12-15 22:02:03 -05:00
if (displayName.length() > 16) {
displayName = displayName.substr(0, 13) + "...";
2025-12-15 20:56:09 -05:00
}
renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str());
// Draw signal strength indicator
std::string signalStr = getSignalStrengthIndicator(network.rssi);
2025-12-15 22:02:03 -05:00
renderer.drawText(UI_FONT_ID, pageWidth - 90, networkY, signalStr.c_str());
// Draw saved indicator (checkmark) for networks with saved passwords
if (network.hasSavedPassword) {
renderer.drawText(UI_FONT_ID, pageWidth - 50, networkY, "+");
}
2025-12-15 20:56:09 -05:00
// Draw lock icon for encrypted networks
if (network.isEncrypted) {
renderer.drawText(UI_FONT_ID, pageWidth - 30, networkY, "*");
}
}
// Draw scroll indicators if needed
if (scrollOffset > 0) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^");
}
if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v");
}
// Show network count
char countStr[32];
snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 45, countStr);
}
// Draw help text
2025-12-15 22:02:03 -05:00
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | * = Encrypted | + = Saved");
2025-12-15 20:56:09 -05:00
}
void WifiScreen::renderPasswordEntry() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
// Draw network name with good spacing from header
std::string networkInfo = "Network: " + selectedSSID;
if (networkInfo.length() > 30) {
networkInfo = networkInfo.substr(0, 27) + "...";
}
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
// Draw keyboard
if (keyboard) {
keyboard->render(58);
}
}
void WifiScreen::renderConnecting() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == WifiScreenState::SCANNING) {
renderer.drawCenteredText(UI_FONT_ID, top, "Scanning...", true, REGULAR);
} else {
renderer.drawCenteredText(READER_FONT_ID, top - 30, "Connecting...", true, BOLD);
std::string ssidInfo = "to " + selectedSSID;
if (ssidInfo.length() > 25) {
ssidInfo = ssidInfo.substr(0, 22) + "...";
}
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
}
}
void WifiScreen::renderConnected() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
2025-12-15 21:23:21 -05:00
const auto top = (pageHeight - height * 4) / 2;
2025-12-15 20:56:09 -05:00
2025-12-15 21:23:21 -05:00
renderer.drawCenteredText(READER_FONT_ID, top - 30, "Connected!", true, BOLD);
2025-12-15 20:56:09 -05:00
std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "...";
}
2025-12-15 21:23:21 -05:00
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
2025-12-15 20:56:09 -05:00
std::string ipInfo = "IP Address: " + connectedIP;
2025-12-15 21:23:21 -05:00
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
// Show web server info
std::string webInfo = "Web: http://" + connectedIP + "/";
renderer.drawCenteredText(UI_FONT_ID, top + 70, webInfo.c_str(), true, REGULAR);
2025-12-15 20:56:09 -05:00
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR);
}
2025-12-15 22:02:03 -05:00
void WifiScreen::renderSavePrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(READER_FONT_ID, top - 40, "Connected!", true, BOLD);
std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "...";
}
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
renderer.drawCenteredText(UI_FONT_ID, top + 40, "Save password for next time?", true, REGULAR);
// Draw Yes/No buttons
const int buttonY = top + 80;
const int buttonWidth = 60;
const int buttonSpacing = 30;
const int totalWidth = buttonWidth * 2 + buttonSpacing;
const int startX = (pageWidth - totalWidth) / 2;
// Draw "Yes" button
if (savePromptSelection == 0) {
renderer.drawText(UI_FONT_ID, startX, buttonY, "[Yes]");
} else {
renderer.drawText(UI_FONT_ID, startX + 4, buttonY, "Yes");
}
// Draw "No" button
if (savePromptSelection == 1) {
renderer.drawText(UI_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]");
} else {
renderer.drawText(UI_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No");
}
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm", true, REGULAR);
}
2025-12-15 20:56:09 -05:00
void WifiScreen::renderConnectionFailed() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 2) / 2;
renderer.drawCenteredText(READER_FONT_ID, top - 20, "Connection Failed", true, BOLD);
renderer.drawCenteredText(UI_FONT_ID, top + 20, connectionError.c_str(), true, REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to go back", true, REGULAR);
}