mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
440 lines
15 KiB
C++
440 lines
15 KiB
C++
#include "KOReaderSyncActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <WiFi.h>
|
|
#include <esp_sntp.h>
|
|
|
|
#include "KOReaderCredentialStore.h"
|
|
#include "KOReaderDocumentId.h"
|
|
#include "MappedInputManager.h"
|
|
#include "activities/network/WifiSelectionActivity.h"
|
|
#include "fontIds.h"
|
|
|
|
namespace {
|
|
void syncTimeWithNTP() {
|
|
// Stop SNTP if already running (can't reconfigure while running)
|
|
if (esp_sntp_enabled()) {
|
|
esp_sntp_stop();
|
|
}
|
|
|
|
// Configure SNTP
|
|
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
|
esp_sntp_setservername(0, "pool.ntp.org");
|
|
esp_sntp_init();
|
|
|
|
// Wait for time to sync (with timeout)
|
|
int retry = 0;
|
|
const int maxRetries = 50; // 5 seconds max
|
|
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
retry++;
|
|
}
|
|
|
|
if (retry < maxRetries) {
|
|
Serial.printf("[%lu] [KOSync] NTP time synced\n", millis());
|
|
} else {
|
|
Serial.printf("[%lu] [KOSync] NTP sync timeout, using fallback\n", millis());
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void KOReaderSyncActivity::taskTrampoline(void* param) {
|
|
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
|
self->displayTaskLoop();
|
|
}
|
|
|
|
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|
exitActivity();
|
|
|
|
if (!success) {
|
|
Serial.printf("[%lu] [KOSync] WiFi connection failed, exiting\n", millis());
|
|
onCancel();
|
|
return;
|
|
}
|
|
|
|
Serial.printf("[%lu] [KOSync] WiFi connected, starting sync\n", millis());
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = SYNCING;
|
|
statusMessage = "Syncing time...";
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
|
|
// Sync time with NTP before making API requests
|
|
syncTimeWithNTP();
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
statusMessage = "Calculating document hash...";
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
|
|
performSync();
|
|
}
|
|
|
|
void KOReaderSyncActivity::performSync() {
|
|
// Calculate document hash based on user's preferred method
|
|
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
|
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
|
} else {
|
|
documentHash = KOReaderDocumentId::calculate(epubPath);
|
|
}
|
|
if (documentHash.empty()) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = SYNC_FAILED;
|
|
statusMessage = "Failed to calculate document hash";
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
Serial.printf("[%lu] [KOSync] Document hash: %s\n", millis(), documentHash.c_str());
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
statusMessage = "Fetching remote progress...";
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
|
|
// Fetch remote progress
|
|
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
|
|
|
if (result == KOReaderSyncClient::NOT_FOUND) {
|
|
// No remote progress - offer to upload
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = NO_REMOTE_PROGRESS;
|
|
hasRemoteProgress = false;
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
if (result != KOReaderSyncClient::OK) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = SYNC_FAILED;
|
|
statusMessage = KOReaderSyncClient::errorString(result);
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Convert remote progress to CrossPoint position
|
|
hasRemoteProgress = true;
|
|
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
|
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, totalPagesInSpine);
|
|
|
|
// Calculate local progress in KOReader format (for display)
|
|
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
|
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = SHOWING_RESULT;
|
|
selectedOption = 0; // Default to "Apply"
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
}
|
|
|
|
void KOReaderSyncActivity::performUpload() {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = UPLOADING;
|
|
statusMessage = "Uploading progress...";
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
|
|
// Convert current position to KOReader format
|
|
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
|
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
|
|
|
|
KOReaderProgress progress;
|
|
progress.document = documentHash;
|
|
progress.progress = koPos.xpath;
|
|
progress.percentage = koPos.percentage;
|
|
|
|
const auto result = KOReaderSyncClient::updateProgress(progress);
|
|
|
|
if (result != KOReaderSyncClient::OK) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = SYNC_FAILED;
|
|
statusMessage = KOReaderSyncClient::errorString(result);
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
state = UPLOAD_COMPLETE;
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
}
|
|
|
|
void KOReaderSyncActivity::onEnter() {
|
|
ActivityWithSubactivity::onEnter();
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
|
|
xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask",
|
|
4096, // Stack size (larger for network operations)
|
|
this, // Parameters
|
|
1, // Priority
|
|
&displayTaskHandle // Task handle
|
|
);
|
|
|
|
// Check for credentials first
|
|
if (!KOREADER_STORE.hasCredentials()) {
|
|
state = NO_CREDENTIALS;
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Turn on WiFi
|
|
Serial.printf("[%lu] [KOSync] Turning on WiFi...\n", millis());
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
// Check if already connected
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.printf("[%lu] [KOSync] Already connected to WiFi\n", millis());
|
|
state = SYNCING;
|
|
statusMessage = "Syncing time...";
|
|
updateRequired = true;
|
|
|
|
// Perform sync directly (will be handled in loop)
|
|
xTaskCreate(
|
|
[](void* param) {
|
|
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
|
// Sync time first
|
|
syncTimeWithNTP();
|
|
xSemaphoreTake(self->renderingMutex, portMAX_DELAY);
|
|
self->statusMessage = "Calculating document hash...";
|
|
xSemaphoreGive(self->renderingMutex);
|
|
self->updateRequired = true;
|
|
self->performSync();
|
|
vTaskDelete(nullptr);
|
|
},
|
|
"SyncTask", 4096, this, 1, nullptr);
|
|
return;
|
|
}
|
|
|
|
// Launch WiFi selection subactivity
|
|
Serial.printf("[%lu] [KOSync] Launching WifiSelectionActivity...\n", millis());
|
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
}
|
|
|
|
void KOReaderSyncActivity::onExit() {
|
|
ActivityWithSubactivity::onExit();
|
|
|
|
// Turn off wifi
|
|
WiFi.disconnect(false);
|
|
delay(100);
|
|
WiFi.mode(WIFI_OFF);
|
|
delay(100);
|
|
|
|
// Wait until not rendering to delete task
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
if (displayTaskHandle) {
|
|
vTaskDelete(displayTaskHandle);
|
|
displayTaskHandle = nullptr;
|
|
}
|
|
vSemaphoreDelete(renderingMutex);
|
|
renderingMutex = nullptr;
|
|
}
|
|
|
|
void KOReaderSyncActivity::displayTaskLoop() {
|
|
while (true) {
|
|
if (updateRequired) {
|
|
updateRequired = false;
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
render();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void KOReaderSyncActivity::render() {
|
|
if (subActivity) {
|
|
return;
|
|
}
|
|
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
|
|
renderer.clearScreen();
|
|
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "KOReader Sync", true, EpdFontFamily::BOLD);
|
|
|
|
if (state == NO_CREDENTIALS) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "No credentials configured", true, EpdFontFamily::BOLD);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, "Set up KOReader account in Settings");
|
|
|
|
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
if (state == SYNCING || state == UPLOADING) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, statusMessage.c_str(), true, EpdFontFamily::BOLD);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
if (state == SHOWING_RESULT) {
|
|
// Show comparison
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 105, "Progress found!", true, EpdFontFamily::BOLD);
|
|
|
|
// Get chapter names from TOC
|
|
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
|
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
|
const std::string remoteChapter = (remoteTocIndex >= 0)
|
|
? epub->getTocItem(remoteTocIndex).title
|
|
: ("Section " + std::to_string(remotePosition.spineIndex + 1));
|
|
const std::string localChapter = (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
|
|
: ("Section " + std::to_string(currentSpineIndex + 1));
|
|
|
|
// Remote progress - chapter and page
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 145, "Remote:", true);
|
|
char remoteChapterStr[128];
|
|
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 170, remoteChapterStr);
|
|
char remotePageStr[64];
|
|
snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1,
|
|
remoteProgress.percentage * 100);
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 195, remotePageStr);
|
|
|
|
if (!remoteProgress.device.empty()) {
|
|
char deviceStr[64];
|
|
snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str());
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 220, deviceStr);
|
|
}
|
|
|
|
// Local progress - chapter and page
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 255, "Local:", true);
|
|
char localChapterStr[128];
|
|
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 280, localChapterStr);
|
|
char localPageStr[64];
|
|
snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine,
|
|
localProgress.percentage * 100);
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 305, localPageStr);
|
|
|
|
// Options
|
|
const int optionY = marginTop + 335;
|
|
const int optionHeight = 30;
|
|
|
|
// Apply option
|
|
if (selectedOption == 0) {
|
|
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
|
|
}
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, optionY, "Apply remote progress", selectedOption != 0);
|
|
|
|
// Upload option
|
|
if (selectedOption == 1) {
|
|
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
|
|
}
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, optionY + optionHeight, "Upload local progress", selectedOption != 1);
|
|
|
|
// Cancel option
|
|
if (selectedOption == 2) {
|
|
renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight);
|
|
}
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft, optionY + optionHeight * 2, "Cancel", selectedOption != 2);
|
|
|
|
const auto labels = mappedInput.mapLabels("", "Select", "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
if (state == NO_REMOTE_PROGRESS) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "No remote progress found", true, EpdFontFamily::BOLD);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, "Upload current position?");
|
|
|
|
const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
if (state == UPLOAD_COMPLETE) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "Progress uploaded!", true, EpdFontFamily::BOLD);
|
|
|
|
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
if (state == SYNC_FAILED) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "Sync failed", true, EpdFontFamily::BOLD);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, statusMessage.c_str());
|
|
|
|
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void KOReaderSyncActivity::loop() {
|
|
if (subActivity) {
|
|
subActivity->loop();
|
|
return;
|
|
}
|
|
|
|
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
onCancel();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state == SHOWING_RESULT) {
|
|
// Navigate options
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
|
selectedOption = (selectedOption + 2) % 3; // Wrap around
|
|
updateRequired = true;
|
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
selectedOption = (selectedOption + 1) % 3;
|
|
updateRequired = true;
|
|
}
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
if (selectedOption == 0) {
|
|
// Apply remote progress
|
|
onSyncComplete(remotePosition.spineIndex, remotePosition.pageNumber);
|
|
} else if (selectedOption == 1) {
|
|
// Upload local progress
|
|
performUpload();
|
|
} else {
|
|
// Cancel
|
|
onCancel();
|
|
}
|
|
}
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
onCancel();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state == NO_REMOTE_PROGRESS) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
// Calculate hash if not done yet
|
|
if (documentHash.empty()) {
|
|
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
|
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
|
} else {
|
|
documentHash = KOReaderDocumentId::calculate(epubPath);
|
|
}
|
|
}
|
|
performUpload();
|
|
}
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
onCancel();
|
|
}
|
|
return;
|
|
}
|
|
}
|