From f365ba6ff0adb460081b65d3ab86d974fd3c4eb2 Mon Sep 17 00:00:00 2001 From: Brendan O'Leary Date: Mon, 15 Dec 2025 20:56:09 -0500 Subject: [PATCH] Connection to WiFi established --- .gitignore | 1 + platformio.ini | 2 +- src/main.cpp | 10 +- src/screens/OnScreenKeyboard.cpp | 307 ++++++++++++++++++++ src/screens/OnScreenKeyboard.h | 126 +++++++++ src/screens/SettingsScreen.cpp | 60 +++- src/screens/SettingsScreen.h | 19 +- src/screens/WifiScreen.cpp | 461 +++++++++++++++++++++++++++++++ src/screens/WifiScreen.h | 78 ++++++ 9 files changed, 1042 insertions(+), 22 deletions(-) create mode 100644 src/screens/OnScreenKeyboard.cpp create mode 100644 src/screens/OnScreenKeyboard.h create mode 100644 src/screens/WifiScreen.cpp create mode 100644 src/screens/WifiScreen.h diff --git a/.gitignore b/.gitignore index 29bccdd..3690b08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .pio .idea .DS_Store +.vscode \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 7998dcb..ece68b6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,5 @@ [platformio] -crosspoint_version = 0.5.1 +crosspoint_version = 0.5.2 default_envs = default [base] diff --git a/src/main.cpp b/src/main.cpp index 7d9286b..27dd68a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "screens/FullScreenMessageScreen.h" #include "screens/SettingsScreen.h" #include "screens/SleepScreen.h" +#include "screens/WifiScreen.h" #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) @@ -167,9 +168,16 @@ void onSelectEpubFile(const std::string& path) { } } +void onGoToSettings(); + +void onGoToWifi() { + exitScreen(); + enterNewScreen(new WifiScreen(renderer, inputManager, onGoToSettings)); +} + void onGoToSettings() { exitScreen(); - enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome)); + enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome, onGoToWifi)); } void onGoHome() { diff --git a/src/screens/OnScreenKeyboard.cpp b/src/screens/OnScreenKeyboard.cpp new file mode 100644 index 0000000..9671fdf --- /dev/null +++ b/src/screens/OnScreenKeyboard.cpp @@ -0,0 +1,307 @@ +#include "OnScreenKeyboard.h" + +#include "config.h" + +// Keyboard layouts - lowercase +const char* const OnScreenKeyboard::keyboard[NUM_ROWS] = { + "`1234567890-=", + "qwertyuiop[]\\", + "asdfghjkl;'", + "zxcvbnm,./", + "^ _____?", + "^ _____ 0 && text.length() > maxLength) { + text = text.substr(0, maxLength); + } +} + +void OnScreenKeyboard::reset(const std::string& newTitle, const std::string& newInitialText) { + if (!newTitle.empty()) { + title = newTitle; + } + text = newInitialText; + selectedRow = 0; + selectedCol = 0; + shiftActive = false; + complete = false; + cancelled = false; +} + +int OnScreenKeyboard::getRowLength(int row) const { + if (row < 0 || row >= NUM_ROWS) return 0; + + // Return actual length of each row based on keyboard layout + switch (row) { + case 0: return 13; // `1234567890-= + case 1: return 13; // qwertyuiop[]backslash + case 2: return 11; // asdfghjkl;' + case 3: return 10; // zxcvbnm,./ + case 4: return 10; // ^, space (5 wide), backspace, OK (2 wide) + default: return 0; + } +} + +char OnScreenKeyboard::getSelectedChar() const { + const char* const* layout = shiftActive ? keyboardShift : keyboard; + + if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0'; + if (selectedCol < 0 || selectedCol >= getRowLength(selectedRow)) return '\0'; + + return layout[selectedRow][selectedCol]; +} + +void OnScreenKeyboard::handleKeyPress() { + // Handle special row (bottom row with shift, space, backspace, done) + if (selectedRow == SHIFT_ROW) { + if (selectedCol == SHIFT_COL) { + // Shift toggle + shiftActive = !shiftActive; + return; + } + + if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { + // Space bar + if (maxLength == 0 || text.length() < maxLength) { + text += ' '; + } + return; + } + + if (selectedCol == BACKSPACE_COL) { + // Backspace + if (!text.empty()) { + text.pop_back(); + } + return; + } + + if (selectedCol >= DONE_COL) { + // Done button + complete = true; + if (onComplete) { + onComplete(text); + } + return; + } + } + + // Regular character + char c = getSelectedChar(); + if (c != '\0' && c != '^' && c != '_' && c != '<') { + if (maxLength == 0 || text.length() < maxLength) { + text += c; + // Auto-disable shift after typing a letter + if (shiftActive && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { + shiftActive = false; + } + } + } +} + +bool OnScreenKeyboard::handleInput() { + if (complete || cancelled) { + return false; + } + + bool handled = false; + + // Navigation + if (inputManager.wasPressed(InputManager::BTN_UP)) { + if (selectedRow > 0) { + selectedRow--; + // Clamp column to valid range for new row + int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + } + handled = true; + } else if (inputManager.wasPressed(InputManager::BTN_DOWN)) { + if (selectedRow < NUM_ROWS - 1) { + selectedRow++; + int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + } + handled = true; + } else if (inputManager.wasPressed(InputManager::BTN_LEFT)) { + if (selectedCol > 0) { + selectedCol--; + } else if (selectedRow > 0) { + // Wrap to previous row + selectedRow--; + selectedCol = getRowLength(selectedRow) - 1; + } + handled = true; + } else if (inputManager.wasPressed(InputManager::BTN_RIGHT)) { + int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol < maxCol) { + selectedCol++; + } else if (selectedRow < NUM_ROWS - 1) { + // Wrap to next row + selectedRow++; + selectedCol = 0; + } + handled = true; + } + + // Selection + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + handleKeyPress(); + handled = true; + } + + // Cancel + if (inputManager.wasPressed(InputManager::BTN_BACK)) { + cancelled = true; + if (onCancel) { + onCancel(); + } + handled = true; + } + + return handled; +} + +void OnScreenKeyboard::render(int startY) const { + const auto pageWidth = GfxRenderer::getScreenWidth(); + + // Draw title + renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR); + + // Draw input field + int inputY = startY + 22; + renderer.drawText(UI_FONT_ID, 10, inputY, "["); + + std::string displayText; + if (isPassword) { + displayText = std::string(text.length(), '*'); + } else { + displayText = text; + } + + // Show cursor at end + displayText += "_"; + + // Truncate if too long for display - use actual character width from font + int charWidth = renderer.getSpaceWidth(UI_FONT_ID); + if (charWidth < 1) charWidth = 8; // Fallback to approximate width + int maxDisplayLen = (pageWidth - 40) / charWidth; + if (displayText.length() > static_cast(maxDisplayLen)) { + displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3); + } + + renderer.drawText(UI_FONT_ID, 20, inputY, displayText.c_str()); + renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]"); + + // Draw keyboard - use compact spacing to fit 5 rows on screen + int keyboardStartY = inputY + 25; + const int keyWidth = 18; + const int keyHeight = 18; + const int keySpacing = 1; + + const char* const* layout = shiftActive ? keyboardShift : keyboard; + + // Calculate left margin to center the longest row (13 keys) + int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); + int leftMargin = (pageWidth - maxRowWidth) / 2; + + for (int row = 0; row < NUM_ROWS; row++) { + int rowY = keyboardStartY + row * (keyHeight + keySpacing); + + // Left-align all rows for consistent navigation + int startX = leftMargin; + + // Handle bottom row (row 4) specially with proper multi-column keys + if (row == 4) { + // Bottom row layout: CAPS (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) + // Total: 11 visual columns, but we use logical positions for selection + + int currentX = startX; + + // CAPS key (logical col 0, spans 2 key widths) + int capsWidth = 2 * keyWidth + keySpacing; + bool capsSelected = (selectedRow == 4 && selectedCol == SHIFT_COL); + if (capsSelected) { + renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); + renderer.drawText(UI_FONT_ID, currentX + capsWidth - 4, rowY, "]"); + } + renderer.drawText(UI_FONT_ID, currentX + 2, rowY, shiftActive ? "CAPS" : "caps"); + currentX += capsWidth + keySpacing; + + // Space bar (logical cols 2-6, spans 5 key widths) + int spaceWidth = 5 * keyWidth + 4 * keySpacing; + bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); + if (spaceSelected) { + renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); + renderer.drawText(UI_FONT_ID, currentX + spaceWidth - 4, rowY, "]"); + } + // Draw centered underscores for space bar + int spaceTextX = currentX + (spaceWidth / 2) - 12; + renderer.drawText(UI_FONT_ID, spaceTextX, rowY, "_____"); + currentX += spaceWidth + keySpacing; + + // Backspace key (logical col 7, spans 2 key widths) + int bsWidth = 2 * keyWidth + keySpacing; + bool bsSelected = (selectedRow == 4 && selectedCol == BACKSPACE_COL); + if (bsSelected) { + renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); + renderer.drawText(UI_FONT_ID, currentX + bsWidth - 4, rowY, "]"); + } + renderer.drawText(UI_FONT_ID, currentX + 6, rowY, "<-"); + currentX += bsWidth + keySpacing; + + // OK button (logical col 9, spans 2 key widths) + int okWidth = 2 * keyWidth + keySpacing; + bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); + if (okSelected) { + renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); + renderer.drawText(UI_FONT_ID, currentX + okWidth - 4, rowY, "]"); + } + renderer.drawText(UI_FONT_ID, currentX + 8, rowY, "OK"); + + } else { + // Regular rows: render each key individually + for (int col = 0; col < getRowLength(row); col++) { + int keyX = startX + col * (keyWidth + keySpacing); + + // Get the character to display + char c = layout[row][col]; + std::string keyLabel(1, c); + + // Draw selection highlight + bool isSelected = (row == selectedRow && col == selectedCol); + + if (isSelected) { + renderer.drawText(UI_FONT_ID, keyX - 2, rowY, "["); + renderer.drawText(UI_FONT_ID, keyX + keyWidth - 4, rowY, "]"); + } + + renderer.drawText(UI_FONT_ID, keyX + 2, rowY, keyLabel.c_str()); + } + } + } + + // Draw help text at absolute bottom of screen (consistent with other screens) + const auto pageHeight = GfxRenderer::getScreenHeight(); + renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK"); +} diff --git a/src/screens/OnScreenKeyboard.h b/src/screens/OnScreenKeyboard.h new file mode 100644 index 0000000..a0c7a15 --- /dev/null +++ b/src/screens/OnScreenKeyboard.h @@ -0,0 +1,126 @@ +#pragma once +#include +#include + +#include +#include + +/** + * Reusable on-screen keyboard component for text input. + * Can be embedded in any screen that needs text entry. + * + * Usage: + * 1. Create an OnScreenKeyboard instance + * 2. Call render() to draw the keyboard + * 3. Call handleInput() to process button presses + * 4. When isComplete() returns true, get the result from getText() + * 5. Call isCancelled() to check if user cancelled input + */ +class OnScreenKeyboard { + public: + // Callback types + using OnCompleteCallback = std::function; + using OnCancelCallback = std::function; + + /** + * Constructor + * @param renderer Reference to the GfxRenderer for drawing + * @param inputManager Reference to InputManager for handling input + * @param title Title to display above the keyboard + * @param initialText Initial text to show in the input field + * @param maxLength Maximum length of input text (0 for unlimited) + * @param isPassword If true, display asterisks instead of actual characters + */ + OnScreenKeyboard(GfxRenderer& renderer, InputManager& inputManager, + const std::string& title = "Enter Text", + const std::string& initialText = "", + size_t maxLength = 0, + bool isPassword = false); + + /** + * Handle button input. Call this in your screen's handleInput(). + * @return true if input was handled, false otherwise + */ + bool handleInput(); + + /** + * Render the keyboard at the specified Y position. + * @param startY Y-coordinate where keyboard rendering starts + */ + void render(int startY) const; + + /** + * Get the current text entered by the user. + */ + const std::string& getText() const { return text; } + + /** + * Set the current text. + */ + void setText(const std::string& newText); + + /** + * Check if the user has completed text entry (pressed OK on Done). + */ + bool isComplete() const { return complete; } + + /** + * Check if the user has cancelled text entry. + */ + bool isCancelled() const { return cancelled; } + + /** + * Reset the keyboard state for reuse. + */ + void reset(const std::string& newTitle = "", const std::string& newInitialText = ""); + + /** + * Set callback for when input is complete. + */ + void setOnComplete(OnCompleteCallback callback) { onComplete = callback; } + + /** + * Set callback for when input is cancelled. + */ + void setOnCancel(OnCancelCallback callback) { onCancel = callback; } + + private: + GfxRenderer& renderer; + InputManager& inputManager; + + std::string title; + std::string text; + size_t maxLength; + bool isPassword; + + // Keyboard state + int selectedRow = 0; + int selectedCol = 0; + bool shiftActive = false; + bool complete = false; + bool cancelled = false; + + // Callbacks + OnCompleteCallback onComplete; + OnCancelCallback onCancel; + + // Keyboard layout + static constexpr int NUM_ROWS = 5; + static constexpr int KEYS_PER_ROW = 13; // Max keys per row (rows 0 and 1 have 13 keys) + static const char* const keyboard[NUM_ROWS]; + static const char* const keyboardShift[NUM_ROWS]; + + // Special key positions (bottom row) + static constexpr int SHIFT_ROW = 4; + static constexpr int SHIFT_COL = 0; + static constexpr int SPACE_ROW = 4; + static constexpr int SPACE_COL = 2; + static constexpr int BACKSPACE_ROW = 4; + static constexpr int BACKSPACE_COL = 7; + static constexpr int DONE_ROW = 4; + static constexpr int DONE_COL = 9; + + char getSelectedChar() const; + void handleKeyPress(); + int getRowLength(int row) const; +}; diff --git a/src/screens/SettingsScreen.cpp b/src/screens/SettingsScreen.cpp index 3e809ce..565742b 100644 --- a/src/screens/SettingsScreen.cpp +++ b/src/screens/SettingsScreen.cpp @@ -8,8 +8,9 @@ // Define the static settings list const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { - {"White Sleep Screen", &CrossPointSettings::whiteSleepScreen}, - {"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}}; + {"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen}, + {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}, + {"WiFi", SettingType::ACTION, nullptr}}; void SettingsScreen::taskTrampoline(void* param) { auto* self = static_cast(param); @@ -45,14 +46,10 @@ void SettingsScreen::onExit() { } void SettingsScreen::handleInput() { - // Check for Confirm button to toggle setting + // Check for Confirm button to toggle/activate setting if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { - // Toggle the current setting - toggleCurrentSetting(); - - // Trigger a redraw of the entire screen - updateRequired = true; - return; // Return early to prevent further processing + activateCurrentSetting(); + return; } // Check for Back button to exit settings @@ -79,15 +76,42 @@ void SettingsScreen::handleInput() { } } +void SettingsScreen::activateCurrentSetting() { + // Validate index + if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + return; + } + + const auto& setting = settingsList[selectedSettingIndex]; + + if (setting.type == SettingType::TOGGLE) { + toggleCurrentSetting(); + // Trigger a redraw of the entire screen + updateRequired = true; + } else if (setting.type == SettingType::ACTION) { + // Handle action settings + if (std::string(setting.name) == "WiFi") { + onGoWifi(); + } + } +} + void SettingsScreen::toggleCurrentSetting() { // Validate index if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { return; } + const auto& setting = settingsList[selectedSettingIndex]; + + // Only toggle if it's a toggle type and has a value pointer + if (setting.type != SettingType::TOGGLE || setting.valuePtr == nullptr) { + return; + } + // Toggle the boolean value using the member pointer - bool currentValue = SETTINGS.*(settingsList[selectedSettingIndex].valuePtr); - SETTINGS.*(settingsList[selectedSettingIndex].valuePtr) = !currentValue; + bool currentValue = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = !currentValue; // Save settings when they change SETTINGS.saveToFile(); @@ -125,14 +149,20 @@ void SettingsScreen::render() const { renderer.drawText(UI_FONT_ID, 5, settingY, ">"); } - // Draw setting name and value + // Draw setting name renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name); - bool value = SETTINGS.*(settingsList[i].valuePtr); - renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); + + // Draw value based on setting type + if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { + bool value = SETTINGS.*(settingsList[i].valuePtr); + renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); + } else if (settingsList[i].type == SettingType::ACTION) { + renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, ">"); + } } // Draw help text - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit"); + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to select, BACK to save & exit"); // Always use standard refresh for settings screen renderer.displayBuffer(); diff --git a/src/screens/SettingsScreen.h b/src/screens/SettingsScreen.h index 8de45b8..e55e1d2 100644 --- a/src/screens/SettingsScreen.h +++ b/src/screens/SettingsScreen.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -11,10 +12,14 @@ class CrossPointSettings; +// Enum to distinguish setting types +enum class SettingType { TOGGLE, ACTION }; + // Structure to hold setting information struct SettingInfo { - const char* name; // Display name of the setting - uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings + const char* name; // Display name of the setting + SettingType type; // Type of setting + uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE) }; class SettingsScreen final : public Screen { @@ -23,19 +28,23 @@ class SettingsScreen final : public Screen { bool updateRequired = false; int selectedSettingIndex = 0; // Currently selected setting const std::function onGoHome; + const std::function onGoWifi; // Static settings list - static constexpr int settingsCount = 2; // Number of settings + static constexpr int settingsCount = 3; // Number of settings static const SettingInfo settingsList[settingsCount]; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; void toggleCurrentSetting(); + void activateCurrentSetting(); public: - explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome) - : Screen(renderer, inputManager), onGoHome(onGoHome) {} + explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onGoHome, + const std::function& onGoWifi) + : Screen(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {} void onEnter() override; void onExit() override; void handleInput() override; diff --git a/src/screens/WifiScreen.cpp b/src/screens/WifiScreen.cpp new file mode 100644 index 0000000..eaad0eb --- /dev/null +++ b/src/screens/WifiScreen.cpp @@ -0,0 +1,461 @@ +#include "WifiScreen.h" + +#include +#include + +#include "config.h" + +void WifiScreen::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void WifiScreen::onEnter() { + renderingMutex = xSemaphoreCreateMutex(); + + // Reset state + selectedNetworkIndex = 0; + networks.clear(); + state = WifiScreenState::SCANNING; + selectedSSID.clear(); + connectedIP.clear(); + connectionError.clear(); + 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(); + + // Don't turn off WiFi if connected + if (WiFi.status() != WL_CONNECTED) { + WiFi.mode(WIFI_OFF); + } + + // 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); + + // 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(networks.size())) { + return; + } + + const auto& network = networks[index]; + selectedSSID = network.ssid; + selectedRequiresPassword = network.isEncrypted; + + 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); + + if (selectedRequiresPassword && keyboard) { + WiFi.begin(selectedSSID.c_str(), keyboard->getText().c_str()); + } 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; + state = WifiScreenState::CONNECTED; + updateRequired = true; + 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; + } + + // 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(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; + 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(i) == selectedNetworkIndex) { + renderer.drawText(UI_FONT_ID, 5, networkY, ">"); + } + + // Draw network name (truncate if too long) + std::string displayName = network.ssid; + if (displayName.length() > 18) { + displayName = displayName.substr(0, 15) + "..."; + } + renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str()); + + // Draw signal strength indicator + std::string signalStr = getSignalStrengthIndicator(network.rssi); + renderer.drawText(UI_FONT_ID, pageWidth - 80, networkY, signalStr.c_str()); + + // 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(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 + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | BACK: Exit | * = Encrypted"); +} + +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); + const auto top = (pageHeight - height * 3) / 2; + + renderer.drawCenteredText(READER_FONT_ID, top - 20, "Connected!", true, BOLD); + + std::string ssidInfo = "Network: " + selectedSSID; + if (ssidInfo.length() > 28) { + ssidInfo = ssidInfo.substr(0, 25) + "..."; + } + renderer.drawCenteredText(UI_FONT_ID, top + 20, ssidInfo.c_str(), true, REGULAR); + + std::string ipInfo = "IP Address: " + connectedIP; + renderer.drawCenteredText(UI_FONT_ID, top + 50, ipInfo.c_str(), true, REGULAR); + + renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR); +} + +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); +} diff --git a/src/screens/WifiScreen.h b/src/screens/WifiScreen.h new file mode 100644 index 0000000..fe97862 --- /dev/null +++ b/src/screens/WifiScreen.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "OnScreenKeyboard.h" +#include "Screen.h" + +// Structure to hold WiFi network information +struct WifiNetworkInfo { + std::string ssid; + int32_t rssi; + bool isEncrypted; +}; + +// WiFi screen states +enum class WifiScreenState { + SCANNING, // Scanning for networks + NETWORK_LIST, // Displaying available networks + PASSWORD_ENTRY, // Entering password for selected network + CONNECTING, // Attempting to connect + CONNECTED, // Successfully connected, showing IP + CONNECTION_FAILED // Connection failed +}; + +class WifiScreen final : public Screen { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + WifiScreenState state = WifiScreenState::SCANNING; + int selectedNetworkIndex = 0; + std::vector networks; + const std::function onGoBack; + + // Selected network for connection + std::string selectedSSID; + bool selectedRequiresPassword = false; + + // On-screen keyboard for password entry + std::unique_ptr keyboard; + + // Connection result + std::string connectedIP; + std::string connectionError; + + // Connection timeout + static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; + unsigned long connectionStartTime = 0; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void renderNetworkList() const; + void renderPasswordEntry() const; + void renderConnecting() const; + void renderConnected() const; + void renderConnectionFailed() const; + + void startWifiScan(); + void processWifiScanResults(); + void selectNetwork(int index); + void attemptConnection(); + void checkConnectionStatus(); + std::string getSignalStrengthIndicator(int32_t rssi) const; + + public: + explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoBack) + : Screen(renderer, inputManager), onGoBack(onGoBack) {} + void onEnter() override; + void onExit() override; + void handleInput() override; +};