feat: Add network credential settings for FTP and HTTP servers, add editable credential changes for hotspot

This commit is contained in:
altsysrq
2026-01-04 16:56:43 -06:00
parent 1aaec3d98d
commit 90e20b49b7
10 changed files with 334 additions and 24 deletions

View File

@@ -18,8 +18,6 @@
namespace {
// AP Mode configuration
constexpr const char* AP_SSID = "CrossPoint-Reader";
constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
constexpr const char* AP_HOSTNAME = "crosspoint";
constexpr uint8_t AP_CHANNEL = 1;
constexpr uint8_t AP_MAX_CONNECTIONS = 4;
@@ -219,11 +217,11 @@ void FileTransferActivity::startAccessPoint() {
// Start soft AP
bool apStarted;
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
if (!SETTINGS.apPassword.empty() && SETTINGS.apPassword.length() >= 8) {
apStarted = WiFi.softAP(SETTINGS.apSsid.c_str(), SETTINGS.apPassword.c_str(), AP_CHANNEL, false, AP_MAX_CONNECTIONS);
} else {
// Open network (no password)
apStarted = WiFi.softAP(AP_SSID, nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
// Open network (no password or password too short)
apStarted = WiFi.softAP(SETTINGS.apSsid.c_str(), nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
}
if (!apStarted) {
@@ -239,10 +237,10 @@ void FileTransferActivity::startAccessPoint() {
char ipStr[16];
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]);
connectedIP = ipStr;
connectedSSID = AP_SSID;
connectedSSID = SETTINGS.apSsid;
Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis());
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), SETTINGS.apSsid.c_str());
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
// Start mDNS for hostname resolution
@@ -492,6 +490,13 @@ void FileTransferActivity::renderHttpServerRunning() const {
// Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:");
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
// Show HTTP credentials at the bottom
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 14, "Browser will ask for credentials:");
std::string httpUserStr = "Username: " + SETTINGS.httpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 15, httpUserStr.c_str());
std::string httpPassStr = "Password: " + SETTINGS.httpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 16, httpPassStr.c_str());
} else {
// STA mode display (original behavior)
const int startY = 65;
@@ -518,6 +523,13 @@ void FileTransferActivity::renderHttpServerRunning() const {
// Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:");
// Show HTTP credentials
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 12, "Browser will ask for credentials:");
std::string httpUserStr = "Username: " + SETTINGS.httpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 13, httpUserStr.c_str());
std::string httpPassStr = "Password: " + SETTINGS.httpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 14, httpPassStr.c_str());
}
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
@@ -542,7 +554,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3,
"or scan QR code with your phone to connect to WiFi.");
// Show QR code for WiFi
std::string wifiConfig = std::string("WIFI:T:WPA;S:") + connectedSSID + ";P:" + "" + ";;";
std::string wifiConfig = std::string("WIFI:T:") +
(SETTINGS.apPassword.empty() ? "" : "WPA") +
";S:" + connectedSSID +
";P:" + SETTINGS.apPassword + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
startY += 6 * 29 + 3 * LINE_SPACING;
@@ -554,8 +569,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 4, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Connect with FTP client:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Username: crosspoint");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, "Password: reader");
std::string ftpUserStr = "Username: " + SETTINGS.ftpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, ftpUserStr.c_str());
std::string ftpPassStr = "Password: " + SETTINGS.ftpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, ftpPassStr.c_str());
} else {
// STA mode display
const int startY = 65;
@@ -576,8 +593,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Use FTP client to connect:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Username: crosspoint");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Password: reader");
std::string ftpUserStr = "Username: " + SETTINGS.ftpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, ftpUserStr.c_str());
std::string ftpPassStr = "Password: " + SETTINGS.ftpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, ftpPassStr.c_str());
// Show QR code for FTP URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 8, ftpInfo);

View File

@@ -0,0 +1,198 @@
#include "CredentialSettingsActivity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "fontIds.h"
namespace {
constexpr int FIELD_COUNT = 6;
const char* FIELD_NAMES[FIELD_COUNT] = {
"FTP Username",
"FTP Password",
"HTTP Username",
"HTTP Password",
"Hotspot SSID",
"Hotspot Password"
};
} // namespace
void CredentialSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<CredentialSettingsActivity*>(param);
self->displayTaskLoop();
}
void CredentialSettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0;
updateRequired = true;
xTaskCreate(&CredentialSettingsActivity::taskTrampoline, "CredentialSettingsTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void CredentialSettingsActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void CredentialSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
selectCurrentField();
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex > 0) ? (selectedIndex - 1) : (FIELD_COUNT - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % FIELD_COUNT;
updateRequired = true;
}
}
void CredentialSettingsActivity::selectCurrentField() {
std::string* targetField = nullptr;
const char* promptText = "";
switch (selectedIndex) {
case 0: // FTP Username
targetField = &SETTINGS.ftpUsername;
promptText = "Enter FTP username:";
break;
case 1: // FTP Password
targetField = &SETTINGS.ftpPassword;
promptText = "Enter FTP password:";
break;
case 2: // HTTP Username
targetField = &SETTINGS.httpUsername;
promptText = "Enter HTTP username:";
break;
case 3: // HTTP Password
targetField = &SETTINGS.httpPassword;
promptText = "Enter HTTP password:";
break;
case 4: // Hotspot SSID
targetField = &SETTINGS.apSsid;
promptText = "Enter hotspot SSID:";
break;
case 5: // Hotspot Password
targetField = &SETTINGS.apPassword;
promptText = "Enter hotspot password (leave empty for open network):";
break;
}
if (targetField) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
bool isPassword = (selectedIndex == 1 || selectedIndex == 3 || selectedIndex == 5); // Password fields
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput,
promptText, // title
*targetField, // initialText
10, // startY
0, // maxLength (0 = unlimited)
isPassword, // isPassword
[this, targetField](const std::string& newValue) {
*targetField = newValue;
SETTINGS.saveToFile();
exitActivity();
updateRequired = true;
},
[this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
}
void CredentialSettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CredentialSettingsActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Network Credentials", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, 40, "Configure server and hotspot credentials");
// Draw selection
renderer.fillRect(0, 70 + selectedIndex * 35 - 2, pageWidth - 1, 35);
// Draw fields
for (int i = 0; i < FIELD_COUNT; i++) {
const int fieldY = 70 + i * 35;
const bool isSelected = (i == selectedIndex);
// Draw field name
renderer.drawText(UI_10_FONT_ID, 20, fieldY, FIELD_NAMES[i], !isSelected);
// Draw current value (masked for passwords)
std::string displayValue;
switch (i) {
case 0: // FTP Username
displayValue = SETTINGS.ftpUsername;
break;
case 1: // FTP Password
displayValue = SETTINGS.ftpPassword.empty() ? "" : std::string(SETTINGS.ftpPassword.length(), '*');
break;
case 2: // Hotspot SSID
displayValue = SETTINGS.apSsid;
break;
case 3: // Hotspot Password
displayValue = SETTINGS.apPassword.empty() ? "(open)" : std::string(SETTINGS.apPassword.length(), '*');
break;
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, displayValue.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, fieldY, displayValue.c_str(), !isSelected);
}
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Edit", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "activities/ActivityWithSubactivity.h"
/**
* CredentialSettingsActivity allows users to configure credentials for:
* - FTP server (username and password)
* - HTTP server (username and password)
* - WiFi Hotspot (SSID and password)
*/
class CredentialSettingsActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedIndex = 0; // Currently selected credential field
const std::function<void()> onGoBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void selectCurrentField();
public:
explicit CredentialSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CredentialSettings", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@@ -3,6 +3,7 @@
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "CredentialSettingsActivity.h"
#include "FolderPickerActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
@@ -11,7 +12,7 @@
// Define the static settings list
namespace {
constexpr int settingsCount = 18;
constexpr int settingsCount = 19;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@@ -51,6 +52,7 @@ const SettingInfo settingsList[settingsCount] = {
{"Root", "Custom", "Last Used"}},
{"Choose Custom Folder", SettingType::ACTION, nullptr, {}},
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
{"Network Credentials", SettingType::ACTION, nullptr, {}},
{"File Transfer Schedule", SettingType::ACTION, nullptr, {}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
@@ -169,6 +171,14 @@ void SettingsActivity::toggleCurrentSetting() {
},
"/")); // Start from root directory
xSemaphoreGive(renderingMutex);
} else if (std::string(setting.name) == "Network Credentials") {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CredentialSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (std::string(setting.name) == "File Transfer Schedule") {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();