mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-19 15:47:40 +03:00
Connection to WiFi established
This commit is contained in:
parent
c262f222de
commit
f365ba6ff0
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.pio
|
.pio
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
@ -1,5 +1,5 @@
|
|||||||
[platformio]
|
[platformio]
|
||||||
crosspoint_version = 0.5.1
|
crosspoint_version = 0.5.2
|
||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
|
|||||||
10
src/main.cpp
10
src/main.cpp
@ -23,6 +23,7 @@
|
|||||||
#include "screens/FullScreenMessageScreen.h"
|
#include "screens/FullScreenMessageScreen.h"
|
||||||
#include "screens/SettingsScreen.h"
|
#include "screens/SettingsScreen.h"
|
||||||
#include "screens/SleepScreen.h"
|
#include "screens/SleepScreen.h"
|
||||||
|
#include "screens/WifiScreen.h"
|
||||||
|
|
||||||
#define SPI_FQ 40000000
|
#define SPI_FQ 40000000
|
||||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
// 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() {
|
void onGoToSettings() {
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome));
|
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome, onGoToWifi));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
|
|||||||
307
src/screens/OnScreenKeyboard.cpp
Normal file
307
src/screens/OnScreenKeyboard.cpp
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
#include "OnScreenKeyboard.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// Keyboard layouts - lowercase
|
||||||
|
const char* const OnScreenKeyboard::keyboard[NUM_ROWS] = {
|
||||||
|
"`1234567890-=",
|
||||||
|
"qwertyuiop[]\\",
|
||||||
|
"asdfghjkl;'",
|
||||||
|
"zxcvbnm,./",
|
||||||
|
"^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard layouts - uppercase/symbols
|
||||||
|
const char* const OnScreenKeyboard::keyboardShift[NUM_ROWS] = {
|
||||||
|
"~!@#$%^&*()_+",
|
||||||
|
"QWERTYUIOP{}|",
|
||||||
|
"ASDFGHJKL:\"",
|
||||||
|
"ZXCVBNM<>?",
|
||||||
|
"^ _____<OK"
|
||||||
|
};
|
||||||
|
|
||||||
|
OnScreenKeyboard::OnScreenKeyboard(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
|
const std::string& title, const std::string& initialText,
|
||||||
|
size_t maxLength, bool isPassword)
|
||||||
|
: renderer(renderer),
|
||||||
|
inputManager(inputManager),
|
||||||
|
title(title),
|
||||||
|
text(initialText),
|
||||||
|
maxLength(maxLength),
|
||||||
|
isPassword(isPassword) {}
|
||||||
|
|
||||||
|
void OnScreenKeyboard::setText(const std::string& newText) {
|
||||||
|
text = newText;
|
||||||
|
if (maxLength > 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<size_t>(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");
|
||||||
|
}
|
||||||
126
src/screens/OnScreenKeyboard.h
Normal file
126
src/screens/OnScreenKeyboard.h
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<void(const std::string&)>;
|
||||||
|
using OnCancelCallback = std::function<void()>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
@ -8,8 +8,9 @@
|
|||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
|
|
||||||
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = {
|
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = {
|
||||||
{"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
|
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
|
||||||
{"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}};
|
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing},
|
||||||
|
{"WiFi", SettingType::ACTION, nullptr}};
|
||||||
|
|
||||||
void SettingsScreen::taskTrampoline(void* param) {
|
void SettingsScreen::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsScreen*>(param);
|
auto* self = static_cast<SettingsScreen*>(param);
|
||||||
@ -45,14 +46,10 @@ void SettingsScreen::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsScreen::handleInput() {
|
void SettingsScreen::handleInput() {
|
||||||
// Check for Confirm button to toggle setting
|
// Check for Confirm button to toggle/activate setting
|
||||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
// Toggle the current setting
|
activateCurrentSetting();
|
||||||
toggleCurrentSetting();
|
return;
|
||||||
|
|
||||||
// Trigger a redraw of the entire screen
|
|
||||||
updateRequired = true;
|
|
||||||
return; // Return early to prevent further processing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Back button to exit settings
|
// 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() {
|
void SettingsScreen::toggleCurrentSetting() {
|
||||||
// Validate index
|
// Validate index
|
||||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||||
return;
|
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
|
// Toggle the boolean value using the member pointer
|
||||||
bool currentValue = SETTINGS.*(settingsList[selectedSettingIndex].valuePtr);
|
bool currentValue = SETTINGS.*(setting.valuePtr);
|
||||||
SETTINGS.*(settingsList[selectedSettingIndex].valuePtr) = !currentValue;
|
SETTINGS.*(setting.valuePtr) = !currentValue;
|
||||||
|
|
||||||
// Save settings when they change
|
// Save settings when they change
|
||||||
SETTINGS.saveToFile();
|
SETTINGS.saveToFile();
|
||||||
@ -125,14 +149,20 @@ void SettingsScreen::render() const {
|
|||||||
renderer.drawText(UI_FONT_ID, 5, settingY, ">");
|
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);
|
renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name);
|
||||||
|
|
||||||
|
// Draw value based on setting type
|
||||||
|
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
||||||
bool value = SETTINGS.*(settingsList[i].valuePtr);
|
bool value = SETTINGS.*(settingsList[i].valuePtr);
|
||||||
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
|
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
|
// 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
|
// Always use standard refresh for settings screen
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -11,10 +12,14 @@
|
|||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
|
// Enum to distinguish setting types
|
||||||
|
enum class SettingType { TOGGLE, ACTION };
|
||||||
|
|
||||||
// Structure to hold setting information
|
// Structure to hold setting information
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
const char* name; // Display name of the setting
|
const char* name; // Display name of the setting
|
||||||
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
|
SettingType type; // Type of setting
|
||||||
|
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE)
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsScreen final : public Screen {
|
class SettingsScreen final : public Screen {
|
||||||
@ -23,19 +28,23 @@ class SettingsScreen final : public Screen {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedSettingIndex = 0; // Currently selected setting
|
int selectedSettingIndex = 0; // Currently selected setting
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
const std::function<void()> onGoWifi;
|
||||||
|
|
||||||
// Static settings list
|
// 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 const SettingInfo settingsList[settingsCount];
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render() const;
|
void render() const;
|
||||||
void toggleCurrentSetting();
|
void toggleCurrentSetting();
|
||||||
|
void activateCurrentSetting();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
: Screen(renderer, inputManager), onGoHome(onGoHome) {}
|
const std::function<void()>& onGoHome,
|
||||||
|
const std::function<void()>& onGoWifi)
|
||||||
|
: Screen(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void handleInput() override;
|
void handleInput() override;
|
||||||
|
|||||||
461
src/screens/WifiScreen.cpp
Normal file
461
src/screens/WifiScreen.cpp
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
#include "WifiScreen.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
void WifiScreen::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<WifiScreen*>(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<int>(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<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;
|
||||||
|
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;
|
||||||
|
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<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
|
||||||
|
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);
|
||||||
|
}
|
||||||
78
src/screens/WifiScreen.h
Normal file
78
src/screens/WifiScreen.h
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<WifiNetworkInfo> networks;
|
||||||
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
|
// Selected network for connection
|
||||||
|
std::string selectedSSID;
|
||||||
|
bool selectedRequiresPassword = false;
|
||||||
|
|
||||||
|
// On-screen keyboard for password entry
|
||||||
|
std::unique_ptr<OnScreenKeyboard> 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<void()>& onGoBack)
|
||||||
|
: Screen(renderer, inputManager), onGoBack(onGoBack) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void handleInput() override;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user