mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-19 07:37:41 +03:00
Compare commits
16 Commits
de3689a7c7
...
e2a2337239
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a2337239 | ||
|
|
05da79f6ad | ||
|
|
e9e6982eea | ||
|
|
fcee7d519c | ||
|
|
9c68d80781 | ||
|
|
2aa1584582 | ||
|
|
2c79ea8705 | ||
|
|
78604d3bda | ||
|
|
225268c09c | ||
|
|
e384bdbfc2 | ||
|
|
1bc30fbf2a | ||
|
|
596e6fad0b | ||
|
|
698ca629b8 | ||
|
|
d4299efaed | ||
|
|
e4f7327719 | ||
|
|
f365ba6ff0 |
BIN
docs/images/wifi/webserver_files.png
Normal file
BIN
docs/images/wifi/webserver_files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
BIN
docs/images/wifi/webserver_homepage.png
Normal file
BIN
docs/images/wifi/webserver_homepage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
docs/images/wifi/webserver_upload.png
Normal file
BIN
docs/images/wifi/webserver_upload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
docs/images/wifi/wifi_connected.jpeg
Normal file
BIN
docs/images/wifi/wifi_connected.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
docs/images/wifi/wifi_networks.jpeg
Normal file
BIN
docs/images/wifi/wifi_networks.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/images/wifi/wifi_password.jpeg
Normal file
BIN
docs/images/wifi/wifi_password.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
272
docs/webserver.md
Normal file
272
docs/webserver.md
Normal file
@ -0,0 +1,272 @@
|
||||
# Web Server Guide
|
||||
|
||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone.
|
||||
|
||||
## Overview
|
||||
|
||||
CrossPoint Reader includes a built-in web server that allows you to:
|
||||
|
||||
- Upload EPUB files wirelessly from any device on the same WiFi network
|
||||
- Browse and manage files on your device's SD card
|
||||
- Create folders to organize your ebooks
|
||||
- Delete files and folders
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Your CrossPoint Reader device
|
||||
- A WiFi network
|
||||
- A computer, phone, or tablet connected to the **same WiFi network**
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Accessing the WiFi Screen
|
||||
|
||||
1. From the main menu or file browser, navigate to the **Settings** screen
|
||||
2. Select the **WiFi** option
|
||||
3. The device will automatically start scanning for available networks
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Connecting to WiFi
|
||||
|
||||
### Viewing Available Networks
|
||||
|
||||
Once the scan completes, you'll see a list of available WiFi networks with the following indicators:
|
||||
|
||||
- **Signal strength bars** (`||||`, `|||`, `||`, `|`) - Shows connection quality
|
||||
- **`*` symbol** - Indicates the network is password-protected (encrypted)
|
||||
- **`+` symbol** - Indicates you have previously saved credentials for this network
|
||||
|
||||
<img src="./images/wifi/wifi_networks.jpeg" height="500">
|
||||
|
||||
### Selecting a Network
|
||||
|
||||
1. Use the **Left/Right** (or **Volume Up/Down**) buttons to navigate through the network list
|
||||
2. Press **Confirm** to select the highlighted network
|
||||
|
||||
### Entering Password (for encrypted networks)
|
||||
|
||||
If the network requires a password:
|
||||
|
||||
1. An on-screen keyboard will appear
|
||||
2. Use the navigation buttons to select characters
|
||||
3. Press **Confirm** to enter each character
|
||||
4. When complete, select the **Done** option on the keyboard
|
||||
|
||||
<img src="./images/wifi/wifi_password.jpeg" height="500">
|
||||
|
||||
**Note:** If you've previously connected to this network, the saved password will be used automatically.
|
||||
|
||||
### Connection Process
|
||||
|
||||
The device will display "Connecting..." while establishing the connection. This typically takes 5-10 seconds.
|
||||
|
||||
### Saving Credentials
|
||||
|
||||
If this is a new network, you'll be prompted to save the password:
|
||||
|
||||
- Select **Yes** to save credentials for automatic connection next time (NOTE: These are stored in plaintext on the device's SD card. Do not use this for sensitive networks.)
|
||||
- Select **No** to connect without saving
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Connection Success
|
||||
|
||||
Once connected, the screen will display:
|
||||
|
||||
- **Network name** (SSID)
|
||||
- **IP Address** (e.g., `192.168.1.102`)
|
||||
- **Web server URL** (e.g., `http://192.168.1.102/`)
|
||||
|
||||
<img src="./images/wifi/wifi_connected.jpeg" height="500">
|
||||
|
||||
**Important:** Make note of the IP address - you'll need this to access the web interface from your computer or phone.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Accessing the Web Interface
|
||||
|
||||
### From a Computer
|
||||
|
||||
1. Ensure your computer is connected to the **same WiFi network** as your CrossPoint Reader
|
||||
2. Open any web browser (Chrome is recommended)
|
||||
3. Type the IP address shown on your device into the browser's address bar
|
||||
- Example: `http://192.168.1.102/`
|
||||
4. Press Enter
|
||||
|
||||
### From a Phone or Tablet
|
||||
|
||||
1. Ensure your phone/tablet is connected to the **same WiFi network** as your CrossPoint Reader
|
||||
2. Open your mobile browser (Safari, Chrome, etc.)
|
||||
3. Type the IP address into the address bar
|
||||
- Example: `http://192.168.1.102/`
|
||||
4. Tap Go
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Using the Web Interface
|
||||
|
||||
### Home Page
|
||||
|
||||
The home page displays:
|
||||
|
||||
- Device status and version information
|
||||
- WiFi connection status
|
||||
- Current IP address
|
||||
- Available memory
|
||||
|
||||
Navigation links:
|
||||
|
||||
- **Home** - Returns to the status page
|
||||
- **File Manager** - Access file management features
|
||||
|
||||
<img src="./images/wifi/webserver_homepage.png" width="600">
|
||||
|
||||
### File Manager
|
||||
|
||||
Click **File Manager** to access file management features.
|
||||
|
||||
#### Browsing Files
|
||||
|
||||
- The file manager displays all files and folders on your SD card
|
||||
- **Folders** are highlighted in yellow with a 📁 icon
|
||||
- **EPUB files** are highlighted in green with a 📗 icon
|
||||
- Click on a folder name to navigate into it
|
||||
- Use the breadcrumb navigation at the top to go back to parent folders
|
||||
|
||||
<img src="./images/wifi/webserver_files.png" width="600">
|
||||
|
||||
#### Uploading EPUB Files
|
||||
|
||||
1. Click the **+ Add** button in the top-right corner
|
||||
2. Select **Upload eBook** from the dropdown menu
|
||||
3. Click **Choose File** and select an `.epub` file from your device
|
||||
4. Click **Upload**
|
||||
5. A progress bar will show the upload status
|
||||
6. The page will automatically refresh when the upload is complete
|
||||
|
||||
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
|
||||
|
||||
<img src="./images/wifi/webserver_upload.png" width="600">
|
||||
|
||||
#### Creating Folders
|
||||
|
||||
1. Click the **+ Add** button in the top-right corner
|
||||
2. Select **New Folder** from the dropdown menu
|
||||
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
|
||||
4. Click **Create Folder**
|
||||
|
||||
This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
#### Deleting Files and Folders
|
||||
|
||||
1. Click the **🗑️** (trash) icon next to any file or folder
|
||||
2. Confirm the deletion in the popup dialog
|
||||
3. Click **Delete** to permanently remove the item
|
||||
|
||||
**Warning:** Deletion is permanent and cannot be undone!
|
||||
|
||||
**Note:** Folders must be empty before they can be deleted.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot See the Device on the Network
|
||||
|
||||
**Problem:** Browser shows "Cannot connect" or "Site can't be reached"
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify both devices are on the **same WiFi network**
|
||||
- Check your computer/phone WiFi settings
|
||||
- Confirm the CrossPoint Reader shows "Connected" status
|
||||
2. Double-check the IP address
|
||||
- Make sure you typed it correctly
|
||||
- Include `http://` at the beginning
|
||||
3. Try disabling VPN if you're using one
|
||||
4. Some networks have "client isolation" enabled - check with your network administrator
|
||||
|
||||
### Connection Drops or Times Out
|
||||
|
||||
**Problem:** WiFi connection is unstable
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Move closer to the WiFi router
|
||||
2. Check signal strength on the device (should be at least `||` or better)
|
||||
3. Avoid interference from other devices
|
||||
4. Try a different WiFi network if available
|
||||
|
||||
### Upload Fails
|
||||
|
||||
**Problem:** File upload doesn't complete or shows an error
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Ensure the file is a valid `.epub` file
|
||||
2. Check that the SD card has enough free space
|
||||
3. Try uploading a smaller file first to test
|
||||
4. Refresh the browser page and try again
|
||||
|
||||
### Saved Password Not Working
|
||||
|
||||
**Problem:** Device fails to connect with saved credentials
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. When connection fails, you'll be prompted to "Forget Network"
|
||||
2. Select **Yes** to remove the saved password
|
||||
3. Reconnect and enter the password again
|
||||
4. Choose to save the new password
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The web server runs on port 80 (standard HTTP)
|
||||
- **No authentication is required** - anyone on the same network can access the interface
|
||||
- The web server is only accessible while the WiFi screen shows "Connected"
|
||||
- The web server automatically stops when you exit the WiFi screen
|
||||
- For security, only use on trusted private networks
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
||||
- **Web Server Port:** 80 (HTTP)
|
||||
- **Maximum Upload Size:** Limited by available SD card space
|
||||
- **Supported File Format:** `.epub` only
|
||||
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
---
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
||||
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
||||
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload
|
||||
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
||||
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
||||
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
||||
|
||||
---
|
||||
|
||||
## Exiting WiFi Mode
|
||||
|
||||
When you're finished uploading files:
|
||||
|
||||
1. Press the **Back** button on your CrossPoint Reader
|
||||
2. The web server will automatically stop
|
||||
3. WiFi will disconnect to conserve battery
|
||||
4. You'll return to the previous screen
|
||||
|
||||
Your uploaded files will be immediately available in the file browser!
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [User Guide](../USER_GUIDE.md) - General device operation
|
||||
- [README](../README.md) - Project overview and features
|
||||
1475
src/CrossPointWebServer.cpp
Normal file
1475
src/CrossPointWebServer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
59
src/CrossPointWebServer.h
Normal file
59
src/CrossPointWebServer.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <WebServer.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Structure to hold file information
|
||||
struct FileInfo {
|
||||
String name;
|
||||
size_t size;
|
||||
bool isEpub;
|
||||
bool isDirectory;
|
||||
};
|
||||
|
||||
class CrossPointWebServer {
|
||||
public:
|
||||
CrossPointWebServer();
|
||||
~CrossPointWebServer();
|
||||
|
||||
// Start the web server (call after WiFi is connected)
|
||||
void begin();
|
||||
|
||||
// Stop the web server
|
||||
void stop();
|
||||
|
||||
// Call this periodically to handle client requests
|
||||
void handleClient();
|
||||
|
||||
// Check if server is running
|
||||
bool isRunning() const { return running; }
|
||||
|
||||
// Get the port number
|
||||
uint16_t getPort() const { return port; }
|
||||
|
||||
private:
|
||||
WebServer* server = nullptr;
|
||||
bool running = false;
|
||||
uint16_t port = 80;
|
||||
|
||||
// File scanning
|
||||
std::vector<FileInfo> scanFiles(const char* path = "/");
|
||||
String formatFileSize(size_t bytes);
|
||||
bool isEpubFile(const String& filename);
|
||||
|
||||
// Request handlers
|
||||
void handleRoot();
|
||||
void handleNotFound();
|
||||
void handleStatus();
|
||||
void handleFileList();
|
||||
void handleUpload();
|
||||
void handleUploadPost();
|
||||
void handleCreateFolder();
|
||||
void handleDelete();
|
||||
};
|
||||
|
||||
// Global instance
|
||||
extern CrossPointWebServer crossPointWebServer;
|
||||
158
src/WifiCredentialStore.cpp
Normal file
158
src/WifiCredentialStore.cpp
Normal file
@ -0,0 +1,158 @@
|
||||
#include "WifiCredentialStore.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SD.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
// Initialize the static instance
|
||||
WifiCredentialStore WifiCredentialStore::instance;
|
||||
|
||||
// File format version
|
||||
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
||||
|
||||
// WiFi credentials file path
|
||||
constexpr char WIFI_FILE[] = "/sd/.crosspoint/wifi.bin";
|
||||
|
||||
// Obfuscation key - "CrossPoint" in ASCII
|
||||
// This is NOT cryptographic security, just prevents casual file reading
|
||||
constexpr uint8_t OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, 0x50, 0x6F, 0x69, 0x6E, 0x74};
|
||||
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
|
||||
|
||||
void WifiCredentialStore::obfuscate(std::string& data) const {
|
||||
Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
|
||||
}
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
SD.mkdir("/.crosspoint");
|
||||
|
||||
std::ofstream file(WIFI_FILE, std::ios::binary);
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [WCS] Failed to open wifi.bin for writing\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write header
|
||||
serialization::writePod(file, WIFI_FILE_VERSION);
|
||||
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
|
||||
|
||||
// Write each credential
|
||||
for (const auto& cred : credentials) {
|
||||
// Write SSID (plaintext - not sensitive)
|
||||
serialization::writeString(file, cred.ssid);
|
||||
Serial.printf("[%lu] [WCS] Saving SSID: %s, password length: %zu\n", millis(), cred.ssid.c_str(),
|
||||
cred.password.size());
|
||||
|
||||
// Write password (obfuscated)
|
||||
std::string obfuscatedPwd = cred.password;
|
||||
obfuscate(obfuscatedPwd);
|
||||
serialization::writeString(file, obfuscatedPwd);
|
||||
}
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::loadFromFile() {
|
||||
if (!SD.exists(WIFI_FILE + 3)) { // +3 to skip "/sd" prefix
|
||||
Serial.printf("[%lu] [WCS] WiFi credentials file does not exist\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream file(WIFI_FILE, std::ios::binary);
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [WCS] Failed to open wifi.bin for reading\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and verify version
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != WIFI_FILE_VERSION) {
|
||||
Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read credential count
|
||||
uint8_t count;
|
||||
serialization::readPod(file, count);
|
||||
|
||||
// Read credentials
|
||||
credentials.clear();
|
||||
for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) {
|
||||
WifiCredential cred;
|
||||
|
||||
// Read SSID
|
||||
serialization::readString(file, cred.ssid);
|
||||
|
||||
// Read and deobfuscate password
|
||||
serialization::readString(file, cred.password);
|
||||
Serial.printf("[%lu] [WCS] Loaded SSID: %s, obfuscated password length: %zu\n", millis(), cred.ssid.c_str(),
|
||||
cred.password.size());
|
||||
obfuscate(cred.password); // XOR is symmetric, so same function deobfuscates
|
||||
Serial.printf("[%lu] [WCS] After deobfuscation, password length: %zu\n", millis(), cred.password.size());
|
||||
|
||||
credentials.push_back(cred);
|
||||
}
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) {
|
||||
// Check if this SSID already exists and update it
|
||||
for (auto& cred : credentials) {
|
||||
if (cred.ssid == ssid) {
|
||||
cred.password = password;
|
||||
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've reached the limit
|
||||
if (credentials.size() >= MAX_NETWORKS) {
|
||||
Serial.printf("[%lu] [WCS] Cannot add more networks, limit of %zu reached\n", millis(), MAX_NETWORKS);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add new credential
|
||||
credentials.push_back({ssid, password});
|
||||
Serial.printf("[%lu] [WCS] Added credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
||||
for (auto it = credentials.begin(); it != credentials.end(); ++it) {
|
||||
if (it->ssid == ssid) {
|
||||
credentials.erase(it);
|
||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
}
|
||||
return false; // Not found
|
||||
}
|
||||
|
||||
const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const {
|
||||
for (const auto& cred : credentials) {
|
||||
if (cred.ssid == ssid) {
|
||||
return &cred;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
||||
|
||||
void WifiCredentialStore::clearAll() {
|
||||
credentials.clear();
|
||||
saveToFile();
|
||||
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
||||
}
|
||||
56
src/WifiCredentialStore.h
Normal file
56
src/WifiCredentialStore.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct WifiCredential {
|
||||
std::string ssid;
|
||||
std::string password; // Stored obfuscated in file
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton class for storing WiFi credentials on the SD card.
|
||||
* Credentials are stored in /sd/.crosspoint/wifi.bin with basic
|
||||
* XOR obfuscation to prevent casual reading (not cryptographically secure).
|
||||
*/
|
||||
class WifiCredentialStore {
|
||||
private:
|
||||
static WifiCredentialStore instance;
|
||||
std::vector<WifiCredential> credentials;
|
||||
|
||||
static constexpr size_t MAX_NETWORKS = 8;
|
||||
|
||||
// Private constructor for singleton
|
||||
WifiCredentialStore() = default;
|
||||
|
||||
// XOR obfuscation (symmetric - same for encode/decode)
|
||||
void obfuscate(std::string& data) const;
|
||||
|
||||
public:
|
||||
// Delete copy constructor and assignment
|
||||
WifiCredentialStore(const WifiCredentialStore&) = delete;
|
||||
WifiCredentialStore& operator=(const WifiCredentialStore&) = delete;
|
||||
|
||||
// Get singleton instance
|
||||
static WifiCredentialStore& getInstance() { return instance; }
|
||||
|
||||
// Save/load from SD card
|
||||
bool saveToFile() const;
|
||||
bool loadFromFile();
|
||||
|
||||
// Credential management
|
||||
bool addCredential(const std::string& ssid, const std::string& password);
|
||||
bool removeCredential(const std::string& ssid);
|
||||
const WifiCredential* findCredential(const std::string& ssid) const;
|
||||
|
||||
// Get all stored credentials (for UI display)
|
||||
const std::vector<WifiCredential>& getCredentials() const { return credentials; }
|
||||
|
||||
// Check if a network is saved
|
||||
bool hasSavedCredential(const std::string& ssid) const;
|
||||
|
||||
// Clear all credentials
|
||||
void clearAll();
|
||||
};
|
||||
|
||||
// Helper macro to access credentials store
|
||||
#define WIFI_STORE WifiCredentialStore::getInstance()
|
||||
17
src/main.cpp
17
src/main.cpp
@ -5,6 +5,7 @@
|
||||
#include <InputManager.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <WiFi.h>
|
||||
#include <builtinFonts/bookerly_2b.h>
|
||||
#include <builtinFonts/bookerly_bold_2b.h>
|
||||
#include <builtinFonts/bookerly_bold_italic_2b.h>
|
||||
@ -16,6 +17,7 @@
|
||||
#include "Battery.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "CrossPointWebServer.h"
|
||||
#include "config.h"
|
||||
#include "screens/BootLogoScreen.h"
|
||||
#include "screens/EpubReaderScreen.h"
|
||||
@ -23,6 +25,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)
|
||||
@ -169,9 +172,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() {
|
||||
@ -238,6 +248,11 @@ void loop() {
|
||||
lastMemPrint = millis();
|
||||
}
|
||||
|
||||
// Handle web server clients if WiFi is connected
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
crossPointWebServer.handleClient();
|
||||
}
|
||||
|
||||
inputManager.update();
|
||||
|
||||
// Check for any user activity (button press or release)
|
||||
|
||||
304
src/screens/OnScreenKeyboard.cpp
Normal file
304
src/screens/OnScreenKeyboard.cpp
Normal file
@ -0,0 +1,304 @@
|
||||
#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 = 3;
|
||||
|
||||
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");
|
||||
}
|
||||
123
src/screens/OnScreenKeyboard.h
Normal file
123
src/screens/OnScreenKeyboard.h
Normal file
@ -0,0 +1,123 @@
|
||||
#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
|
||||
|
||||
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<SettingsScreen*>(param);
|
||||
@ -47,7 +48,7 @@ void SettingsScreen::onExit() {
|
||||
void SettingsScreen::handleInput() {
|
||||
// Handle actions with early return
|
||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
toggleCurrentSetting();
|
||||
activateCurrentSetting();
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
@ -64,9 +65,31 @@ void SettingsScreen::handleInput() {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
||||
updateRequired = true;
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
|
||||
// Move selection down (with wrap-around)
|
||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||
// Move selection down
|
||||
if (selectedSettingIndex < settingsCount - 1) {
|
||||
selectedSettingIndex++;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,9 +99,16 @@ void SettingsScreen::toggleCurrentSetting() {
|
||||
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();
|
||||
@ -116,14 +146,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();
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@ -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
|
||||
SettingType type; // Type of setting
|
||||
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE)
|
||||
};
|
||||
|
||||
class SettingsScreen final : public Screen {
|
||||
@ -23,19 +28,22 @@ class SettingsScreen final : public Screen {
|
||||
bool updateRequired = false;
|
||||
int selectedSettingIndex = 0; // Currently selected setting
|
||||
const std::function<void()> onGoHome;
|
||||
const std::function<void()> 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<void()>& onGoHome)
|
||||
: Screen(renderer, inputManager), onGoHome(onGoHome) {}
|
||||
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome,
|
||||
const std::function<void()>& onGoWifi)
|
||||
: Screen(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput() override;
|
||||
|
||||
668
src/screens/WifiScreen.cpp
Normal file
668
src/screens/WifiScreen.cpp
Normal file
@ -0,0 +1,668 @@
|
||||
#include "WifiScreen.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "CrossPointWebServer.h"
|
||||
#include "WifiCredentialStore.h"
|
||||
#include "config.h"
|
||||
|
||||
void WifiScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<WifiScreen*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void WifiScreen::onEnter() {
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Load saved WiFi credentials
|
||||
WIFI_STORE.loadFromFile();
|
||||
|
||||
// Reset state
|
||||
selectedNetworkIndex = 0;
|
||||
networks.clear();
|
||||
state = WifiScreenState::SCANNING;
|
||||
selectedSSID.clear();
|
||||
connectedIP.clear();
|
||||
connectionError.clear();
|
||||
enteredPassword.clear();
|
||||
usedSavedPassword = false;
|
||||
savePromptSelection = 0;
|
||||
forgetPromptSelection = 0;
|
||||
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();
|
||||
|
||||
// Stop the web server to free memory
|
||||
crossPointWebServer.stop();
|
||||
|
||||
// Disconnect WiFi to free memory
|
||||
WiFi.disconnect(true);
|
||||
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);
|
||||
network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid);
|
||||
|
||||
// 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;
|
||||
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;
|
||||
Serial.printf("[%lu] [WiFi] Using saved password for %s, length: %zu\n", millis(), selectedSSID.c_str(),
|
||||
enteredPassword.size());
|
||||
attemptConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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());
|
||||
} 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;
|
||||
|
||||
// Start the web server
|
||||
crossPointWebServer.begin();
|
||||
|
||||
// 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;
|
||||
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 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;
|
||||
}
|
||||
|
||||
// Handle forget prompt state (connection failed with saved credentials)
|
||||
if (state == WifiScreenState::FORGET_PROMPT) {
|
||||
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
|
||||
if (forgetPromptSelection > 0) {
|
||||
forgetPromptSelection--;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || inputManager.wasPressed(InputManager::BTN_DOWN)) {
|
||||
if (forgetPromptSelection < 1) {
|
||||
forgetPromptSelection++;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
if (forgetPromptSelection == 0) {
|
||||
// User chose "Yes" - forget the network
|
||||
WIFI_STORE.removeCredential(selectedSSID);
|
||||
// Update the network list to reflect the change
|
||||
for (auto& network : networks) {
|
||||
if (network.ssid == selectedSSID) {
|
||||
network.hasSavedPassword = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Go back to network list
|
||||
state = WifiScreenState::NETWORK_LIST;
|
||||
updateRequired = true;
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||
// Skip forgetting, go back to network list
|
||||
state = WifiScreenState::NETWORK_LIST;
|
||||
updateRequired = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle connected state
|
||||
if (state == WifiScreenState::CONNECTED) {
|
||||
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
// Exit screen on success
|
||||
onGoBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle connection failed state
|
||||
if (state == WifiScreenState::CONNECTION_FAILED) {
|
||||
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
// If we used saved credentials, offer to forget the network
|
||||
if (usedSavedPassword) {
|
||||
state = WifiScreenState::FORGET_PROMPT;
|
||||
forgetPromptSelection = 0; // Default to "Yes"
|
||||
} else {
|
||||
// Go back to network list on failure
|
||||
state = WifiScreenState::NETWORK_LIST;
|
||||
}
|
||||
updateRequired = true;
|
||||
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::SAVE_PROMPT:
|
||||
renderSavePrompt();
|
||||
break;
|
||||
case WifiScreenState::CONNECTION_FAILED:
|
||||
renderConnectionFailed();
|
||||
break;
|
||||
case WifiScreenState::FORGET_PROMPT:
|
||||
renderForgetPrompt();
|
||||
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() > 16) {
|
||||
displayName = displayName.substr(0, 13) + "...";
|
||||
}
|
||||
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 - 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, "+");
|
||||
}
|
||||
|
||||
// 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 | * = Encrypted | + = Saved");
|
||||
}
|
||||
|
||||
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 * 4) / 2;
|
||||
|
||||
renderer.drawCenteredText(READER_FONT_ID, top - 30, "Connected!", true, BOLD);
|
||||
|
||||
std::string ssidInfo = "Network: " + selectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo = ssidInfo.substr(0, 25) + "...";
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
|
||||
|
||||
std::string ipInfo = "IP Address: " + connectedIP;
|
||||
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);
|
||||
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to exit", true, REGULAR);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 continue", true, REGULAR);
|
||||
}
|
||||
|
||||
void WifiScreen::renderForgetPrompt() 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, "Forget Network?", 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, "Remove saved password?", 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 (forgetPromptSelection == 0) {
|
||||
renderer.drawText(UI_FONT_ID, startX, buttonY, "[Yes]");
|
||||
} else {
|
||||
renderer.drawText(UI_FONT_ID, startX + 4, buttonY, "Yes");
|
||||
}
|
||||
|
||||
// Draw "No" button
|
||||
if (forgetPromptSelection == 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);
|
||||
}
|
||||
93
src/screens/WifiScreen.h
Normal file
93
src/screens/WifiScreen.h
Normal file
@ -0,0 +1,93 @@
|
||||
#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;
|
||||
bool hasSavedPassword; // Whether we have saved credentials for this network
|
||||
};
|
||||
|
||||
// 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
|
||||
SAVE_PROMPT, // Asking user if they want to save the password
|
||||
CONNECTION_FAILED, // Connection failed
|
||||
FORGET_PROMPT // Asking user if they want to forget the network
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// Password to potentially save (from keyboard or saved credentials)
|
||||
std::string enteredPassword;
|
||||
|
||||
// Whether network was connected using a saved password (skip save prompt)
|
||||
bool usedSavedPassword = false;
|
||||
|
||||
// Save/forget prompt selection (0 = Yes, 1 = No)
|
||||
int savePromptSelection = 0;
|
||||
int forgetPromptSelection = 0;
|
||||
|
||||
// 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 renderSavePrompt() const;
|
||||
void renderConnectionFailed() const;
|
||||
void renderForgetPrompt() 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