mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
fixes crash on connection
This commit is contained in:
parent
56ec3dfb6d
commit
0fa57a6bb4
@ -45,6 +45,11 @@ void CalibreWirelessActivity::onEnter() {
|
|||||||
bytesReceived = 0;
|
bytesReceived = 0;
|
||||||
inBinaryMode = false;
|
inBinaryMode = false;
|
||||||
recvBuffer.clear();
|
recvBuffer.clear();
|
||||||
|
inSkipMode = false;
|
||||||
|
skipBytesRemaining = 0;
|
||||||
|
skipOpcode = -1;
|
||||||
|
skipExtractedLpath.clear();
|
||||||
|
skipExtractedLength = 0;
|
||||||
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
@ -61,6 +66,20 @@ void CalibreWirelessActivity::onEnter() {
|
|||||||
void CalibreWirelessActivity::onExit() {
|
void CalibreWirelessActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
|
// Stop network task FIRST before touching any shared state
|
||||||
|
// This prevents the task from accessing members while we clean up
|
||||||
|
if (networkTaskHandle) {
|
||||||
|
vTaskDelete(networkTaskHandle);
|
||||||
|
networkTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop display task
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now safe to clean up - tasks are stopped
|
||||||
// Turn off WiFi when exiting
|
// Turn off WiFi when exiting
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
|
|
||||||
@ -77,25 +96,22 @@ void CalibreWirelessActivity::onExit() {
|
|||||||
currentFile.close();
|
currentFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire stateMutex before deleting network task to avoid race condition
|
// Clear string buffers to free memory
|
||||||
xSemaphoreTake(stateMutex, portMAX_DELAY);
|
recvBuffer.clear();
|
||||||
if (networkTaskHandle) {
|
recvBuffer.shrink_to_fit();
|
||||||
vTaskDelete(networkTaskHandle);
|
skipExtractedLpath.clear();
|
||||||
networkTaskHandle = nullptr;
|
skipExtractedLpath.shrink_to_fit();
|
||||||
}
|
|
||||||
xSemaphoreGive(stateMutex);
|
|
||||||
|
|
||||||
// Acquire renderingMutex before deleting display task
|
// Delete mutexes last
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
if (renderingMutex) {
|
||||||
if (displayTaskHandle) {
|
vSemaphoreDelete(renderingMutex);
|
||||||
vTaskDelete(displayTaskHandle);
|
renderingMutex = nullptr;
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
}
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
|
|
||||||
vSemaphoreDelete(stateMutex);
|
if (stateMutex) {
|
||||||
stateMutex = nullptr;
|
vSemaphoreDelete(stateMutex);
|
||||||
|
stateMutex = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibreWirelessActivity::loop() {
|
void CalibreWirelessActivity::loop() {
|
||||||
@ -182,28 +198,32 @@ void CalibreWirelessActivity::listenForDiscovery() {
|
|||||||
std::string portStr;
|
std::string portStr;
|
||||||
if (commaPos != std::string::npos && commaPos > semiPos) {
|
if (commaPos != std::string::npos && commaPos > semiPos) {
|
||||||
portStr = response.substr(semiPos + 1, commaPos - semiPos - 1);
|
portStr = response.substr(semiPos + 1, commaPos - semiPos - 1);
|
||||||
// Get alternative port after comma
|
// Get alternative port after comma - parse safely
|
||||||
std::string altPortStr = response.substr(commaPos + 1);
|
uint16_t altPort = 0;
|
||||||
// Trim whitespace and non-digits from alt port
|
for (size_t i = commaPos + 1; i < response.size(); i++) {
|
||||||
size_t altEnd = 0;
|
char c = response[i];
|
||||||
while (altEnd < altPortStr.size() && altPortStr[altEnd] >= '0' && altPortStr[altEnd] <= '9') {
|
if (c >= '0' && c <= '9') {
|
||||||
altEnd++;
|
altPort = altPort * 10 + (c - '0');
|
||||||
}
|
} else {
|
||||||
if (altEnd > 0) {
|
break;
|
||||||
calibreAltPort = static_cast<uint16_t>(std::stoi(altPortStr.substr(0, altEnd)));
|
}
|
||||||
}
|
}
|
||||||
|
calibreAltPort = altPort;
|
||||||
} else {
|
} else {
|
||||||
portStr = response.substr(semiPos + 1);
|
portStr = response.substr(semiPos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim whitespace from main port
|
// Parse main port safely
|
||||||
while (!portStr.empty() && (portStr[0] == ' ' || portStr[0] == '\t')) {
|
uint16_t mainPort = 0;
|
||||||
portStr = portStr.substr(1);
|
for (size_t i = 0; i < portStr.size(); i++) {
|
||||||
}
|
char c = portStr[i];
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
if (!portStr.empty()) {
|
mainPort = mainPort * 10 + (c - '0');
|
||||||
calibrePort = static_cast<uint16_t>(std::stoi(portStr));
|
} else if (c != ' ' && c != '\t') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
calibrePort = mainPort;
|
||||||
|
|
||||||
// Get hostname if present, otherwise use sender IP
|
// Get hostname if present, otherwise use sender IP
|
||||||
if (onPos != std::string::npos && closePos != std::string::npos && closePos > onPos + 4) {
|
if (onPos != std::string::npos && closePos != std::string::npos && closePos > onPos + 4) {
|
||||||
@ -276,7 +296,16 @@ void CalibreWirelessActivity::handleTcpClient() {
|
|||||||
start++;
|
start++;
|
||||||
size_t end = message.find(',', start);
|
size_t end = message.find(',', start);
|
||||||
if (end != std::string::npos) {
|
if (end != std::string::npos) {
|
||||||
const int opcodeInt = std::stoi(message.substr(start, end - start));
|
// Parse opcode safely without exceptions
|
||||||
|
int opcodeInt = 0;
|
||||||
|
for (size_t i = start; i < end; i++) {
|
||||||
|
char c = message[i];
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
opcodeInt = opcodeInt * 10 + (c - '0');
|
||||||
|
} else if (c != ' ' && c != '\t') {
|
||||||
|
break; // Invalid character
|
||||||
|
}
|
||||||
|
}
|
||||||
if (opcodeInt < 0 || opcodeInt >= OpCode::ERROR) {
|
if (opcodeInt < 0 || opcodeInt >= OpCode::ERROR) {
|
||||||
Serial.printf("[%lu] [CAL] Invalid opcode: %d\n", millis(), opcodeInt);
|
Serial.printf("[%lu] [CAL] Invalid opcode: %d\n", millis(), opcodeInt);
|
||||||
sendJsonResponse(OpCode::OK, "{}");
|
sendJsonResponse(OpCode::OK, "{}");
|
||||||
@ -288,8 +317,11 @@ void CalibreWirelessActivity::handleTcpClient() {
|
|||||||
size_t dataStart = end + 1;
|
size_t dataStart = end + 1;
|
||||||
size_t dataEnd = message.rfind(']');
|
size_t dataEnd = message.rfind(']');
|
||||||
std::string data = "";
|
std::string data = "";
|
||||||
if (dataEnd != std::string::npos && dataEnd > dataStart) {
|
if (dataEnd != std::string::npos && dataEnd > dataStart && dataStart < message.size()) {
|
||||||
data = message.substr(dataStart, dataEnd - dataStart);
|
size_t len = dataEnd - dataStart;
|
||||||
|
if (dataStart + len <= message.size()) {
|
||||||
|
data = message.substr(dataStart, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommand(opcode, data);
|
handleCommand(opcode, data);
|
||||||
@ -299,26 +331,71 @@ void CalibreWirelessActivity::handleTcpClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CalibreWirelessActivity::readJsonMessage(std::string& message) {
|
bool CalibreWirelessActivity::readJsonMessage(std::string& message) {
|
||||||
// Read available data into buffer
|
// Maximum message size we'll buffer in memory
|
||||||
int available = tcpClient.available();
|
// Messages larger than this (typically due to base64 covers) are streamed through
|
||||||
if (available > 0) {
|
constexpr size_t MAX_BUFFERED_MSG_SIZE = 32768;
|
||||||
// Limit buffer growth to prevent memory issues
|
|
||||||
if (recvBuffer.size() > 100000) {
|
// If in skip mode, consume bytes until we've skipped the full message
|
||||||
recvBuffer.clear();
|
if (inSkipMode) {
|
||||||
return false;
|
while (skipBytesRemaining > 0) {
|
||||||
}
|
int available = tcpClient.available();
|
||||||
// Read in chunks
|
if (available <= 0) {
|
||||||
char buf[1024];
|
return false; // Need more data
|
||||||
while (available > 0) {
|
}
|
||||||
int toRead = std::min(available, static_cast<int>(sizeof(buf)));
|
|
||||||
int bytesRead = tcpClient.read(reinterpret_cast<uint8_t*>(buf), toRead);
|
// Read and discard in chunks
|
||||||
|
uint8_t discardBuf[1024];
|
||||||
|
size_t toRead = std::min({static_cast<size_t>(available), sizeof(discardBuf), skipBytesRemaining});
|
||||||
|
int bytesRead = tcpClient.read(discardBuf, toRead);
|
||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
recvBuffer.append(buf, bytesRead);
|
skipBytesRemaining -= bytesRead;
|
||||||
available -= bytesRead;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipBytesRemaining == 0) {
|
||||||
|
// Skip complete - if this was SEND_BOOK, construct minimal message
|
||||||
|
inSkipMode = false;
|
||||||
|
if (skipOpcode == OpCode::SEND_BOOK && !skipExtractedLpath.empty() && skipExtractedLength > 0) {
|
||||||
|
// Build minimal JSON that handleSendBook can parse
|
||||||
|
message = "[" + std::to_string(skipOpcode) + ",{\"lpath\":\"" + skipExtractedLpath +
|
||||||
|
"\",\"length\":" + std::to_string(skipExtractedLength) + "}]";
|
||||||
|
skipOpcode = -1;
|
||||||
|
skipExtractedLpath.clear();
|
||||||
|
skipExtractedLength = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// For other opcodes, just acknowledge
|
||||||
|
if (skipOpcode >= 0) {
|
||||||
|
message = "[" + std::to_string(skipOpcode) + ",{}]";
|
||||||
|
skipOpcode = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read available data into buffer (limited to prevent memory issues)
|
||||||
|
int available = tcpClient.available();
|
||||||
|
if (available > 0) {
|
||||||
|
// Only buffer up to a reasonable amount while looking for length prefix
|
||||||
|
size_t maxBuffer = MAX_BUFFERED_MSG_SIZE + 20; // +20 for length prefix digits
|
||||||
|
if (recvBuffer.size() < maxBuffer) {
|
||||||
|
char buf[1024];
|
||||||
|
size_t spaceLeft = maxBuffer - recvBuffer.size();
|
||||||
|
while (available > 0 && spaceLeft > 0) {
|
||||||
|
int toRead = std::min({available, static_cast<int>(sizeof(buf)), static_cast<int>(spaceLeft)});
|
||||||
|
int bytesRead = tcpClient.read(reinterpret_cast<uint8_t*>(buf), toRead);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
recvBuffer.append(buf, bytesRead);
|
||||||
|
available -= bytesRead;
|
||||||
|
spaceLeft -= bytesRead;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recvBuffer.empty()) {
|
if (recvBuffer.empty()) {
|
||||||
@ -328,61 +405,174 @@ bool CalibreWirelessActivity::readJsonMessage(std::string& message) {
|
|||||||
// Find '[' which marks the start of JSON
|
// Find '[' which marks the start of JSON
|
||||||
size_t bracketPos = recvBuffer.find('[');
|
size_t bracketPos = recvBuffer.find('[');
|
||||||
if (bracketPos == std::string::npos) {
|
if (bracketPos == std::string::npos) {
|
||||||
// No '[' found - if buffer is getting large, something is wrong
|
|
||||||
if (recvBuffer.size() > 1000) {
|
if (recvBuffer.size() > 1000) {
|
||||||
recvBuffer.clear();
|
recvBuffer.clear();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract length from digits before '['
|
// Parse length prefix (digits before '[')
|
||||||
// Calibre ALWAYS sends a length prefix, so if it's not valid digits, it's garbage
|
|
||||||
size_t msgLen = 0;
|
size_t msgLen = 0;
|
||||||
bool validPrefix = false;
|
bool validPrefix = false;
|
||||||
|
|
||||||
if (bracketPos > 0 && bracketPos <= 12) {
|
if (bracketPos > 0 && bracketPos <= 12) {
|
||||||
// Check if prefix is all digits
|
|
||||||
bool allDigits = true;
|
bool allDigits = true;
|
||||||
|
size_t parsedLen = 0;
|
||||||
for (size_t i = 0; i < bracketPos; i++) {
|
for (size_t i = 0; i < bracketPos; i++) {
|
||||||
char c = recvBuffer[i];
|
char c = recvBuffer[i];
|
||||||
if (c < '0' || c > '9') {
|
if (c >= '0' && c <= '9') {
|
||||||
|
parsedLen = parsedLen * 10 + (c - '0');
|
||||||
|
} else {
|
||||||
allDigits = false;
|
allDigits = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allDigits) {
|
if (allDigits) {
|
||||||
msgLen = std::stoul(recvBuffer.substr(0, bracketPos));
|
msgLen = parsedLen;
|
||||||
validPrefix = true;
|
validPrefix = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validPrefix) {
|
if (!validPrefix) {
|
||||||
// Not a valid length prefix - discard everything up to '[' and treat '[' as start
|
if (bracketPos > 0 && bracketPos < recvBuffer.size()) {
|
||||||
if (bracketPos > 0) {
|
|
||||||
recvBuffer = recvBuffer.substr(bracketPos);
|
recvBuffer = recvBuffer.substr(bracketPos);
|
||||||
}
|
}
|
||||||
// Without length prefix, we can't reliably parse - wait for more data
|
|
||||||
// that hopefully starts with a proper length prefix
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check the message length
|
// Sanity check - reject absurdly large messages
|
||||||
if (msgLen > 1000000) {
|
if (msgLen > 10000000) {
|
||||||
recvBuffer = recvBuffer.substr(bracketPos + 1); // Skip past this '[' and try again
|
Serial.printf("[%lu] [CAL] Rejecting message with length %zu (too large)\n", millis(), msgLen);
|
||||||
|
recvBuffer.clear();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have the complete message
|
// For large messages, extract essential fields then skip the rest
|
||||||
|
if (msgLen > MAX_BUFFERED_MSG_SIZE) {
|
||||||
|
Serial.printf("[%lu] [CAL] Large message detected (%zu bytes), streaming\n", millis(), msgLen);
|
||||||
|
|
||||||
|
// We need to extract: opcode, and for SEND_BOOK: lpath and length
|
||||||
|
// These fields appear early in the JSON before the large cover data
|
||||||
|
|
||||||
|
// Parse opcode from what we have buffered
|
||||||
|
int opcodeInt = -1;
|
||||||
|
size_t opcodeStart = bracketPos + 1;
|
||||||
|
size_t commaPos = recvBuffer.find(',', opcodeStart);
|
||||||
|
if (commaPos != std::string::npos && commaPos < recvBuffer.size()) {
|
||||||
|
opcodeInt = 0;
|
||||||
|
for (size_t i = opcodeStart; i < commaPos; i++) {
|
||||||
|
char c = recvBuffer[i];
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
opcodeInt = opcodeInt * 10 + (c - '0');
|
||||||
|
} else if (c != ' ' && c != '\t') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipOpcode = opcodeInt;
|
||||||
|
skipExtractedLpath.clear();
|
||||||
|
skipExtractedLength = 0;
|
||||||
|
|
||||||
|
// For SEND_BOOK, try to extract lpath and length from buffered data
|
||||||
|
if (opcodeInt == OpCode::SEND_BOOK) {
|
||||||
|
// Extract lpath
|
||||||
|
size_t lpathPos = recvBuffer.find("\"lpath\"");
|
||||||
|
if (lpathPos != std::string::npos && lpathPos + 7 < recvBuffer.size()) {
|
||||||
|
size_t colonPos = recvBuffer.find(':', lpathPos + 7);
|
||||||
|
if (colonPos != std::string::npos && colonPos + 1 < recvBuffer.size()) {
|
||||||
|
size_t quoteStart = recvBuffer.find('"', colonPos + 1);
|
||||||
|
if (quoteStart != std::string::npos && quoteStart + 1 < recvBuffer.size()) {
|
||||||
|
size_t quoteEnd = recvBuffer.find('"', quoteStart + 1);
|
||||||
|
if (quoteEnd != std::string::npos && quoteEnd > quoteStart + 1) {
|
||||||
|
skipExtractedLpath = recvBuffer.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract top-level length (track depth to skip nested length fields in cover metadata)
|
||||||
|
// Message format is [opcode, {data}], so depth 2 = top level of data object
|
||||||
|
int depth = 0;
|
||||||
|
const char* lengthKey = "\"length\"";
|
||||||
|
const size_t keyLen = 8;
|
||||||
|
for (size_t i = bracketPos; i < recvBuffer.size() && i < bracketPos + 2000; i++) {
|
||||||
|
char c = recvBuffer[i];
|
||||||
|
if (c == '{' || c == '[') {
|
||||||
|
depth++;
|
||||||
|
} else if (c == '}' || c == ']') {
|
||||||
|
depth--;
|
||||||
|
} else if (depth == 2 && c == '"' && i + keyLen <= recvBuffer.size()) {
|
||||||
|
bool match = true;
|
||||||
|
for (size_t j = 0; j < keyLen && match; j++) {
|
||||||
|
if (recvBuffer[i + j] != lengthKey[j]) match = false;
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
size_t numStart = i + keyLen;
|
||||||
|
while (numStart < recvBuffer.size() && (recvBuffer[numStart] == ':' || recvBuffer[numStart] == ' ')) {
|
||||||
|
numStart++;
|
||||||
|
}
|
||||||
|
while (numStart < recvBuffer.size() && recvBuffer[numStart] >= '0' && recvBuffer[numStart] <= '9') {
|
||||||
|
skipExtractedLength = skipExtractedLength * 10 + (recvBuffer[numStart] - '0');
|
||||||
|
numStart++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how many bytes we still need to skip
|
||||||
|
size_t totalMsgBytes = bracketPos + msgLen;
|
||||||
|
size_t alreadyBuffered = recvBuffer.size();
|
||||||
|
if (alreadyBuffered >= totalMsgBytes) {
|
||||||
|
// Entire message is already buffered - just discard it
|
||||||
|
recvBuffer = recvBuffer.substr(totalMsgBytes);
|
||||||
|
skipBytesRemaining = 0;
|
||||||
|
} else {
|
||||||
|
// Need to skip remaining bytes from network
|
||||||
|
skipBytesRemaining = totalMsgBytes - alreadyBuffered;
|
||||||
|
recvBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
inSkipMode = true;
|
||||||
|
|
||||||
|
// If skip is already complete, return immediately
|
||||||
|
if (skipBytesRemaining == 0) {
|
||||||
|
inSkipMode = false;
|
||||||
|
if (skipOpcode == OpCode::SEND_BOOK && !skipExtractedLpath.empty() && skipExtractedLength > 0) {
|
||||||
|
message = "[" + std::to_string(skipOpcode) + ",{\"lpath\":\"" + skipExtractedLpath +
|
||||||
|
"\",\"length\":" + std::to_string(skipExtractedLength) + "}]";
|
||||||
|
skipOpcode = -1;
|
||||||
|
skipExtractedLpath.clear();
|
||||||
|
skipExtractedLength = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (skipOpcode >= 0) {
|
||||||
|
message = "[" + std::to_string(skipOpcode) + ",{}]";
|
||||||
|
skipOpcode = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false; // Continue skipping in next iteration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal path for small messages
|
||||||
size_t totalNeeded = bracketPos + msgLen;
|
size_t totalNeeded = bracketPos + msgLen;
|
||||||
if (recvBuffer.size() < totalNeeded) {
|
if (recvBuffer.size() < totalNeeded) {
|
||||||
// Not enough data yet - wait for more
|
return false; // Wait for more data
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the message
|
// Extract the message
|
||||||
message = recvBuffer.substr(bracketPos, msgLen);
|
if (bracketPos < recvBuffer.size() && bracketPos + msgLen <= recvBuffer.size()) {
|
||||||
|
message = recvBuffer.substr(bracketPos, msgLen);
|
||||||
|
} else {
|
||||||
|
recvBuffer.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the rest in buffer (may contain binary data or next message)
|
// Keep remainder in buffer
|
||||||
if (recvBuffer.size() > totalNeeded) {
|
if (recvBuffer.size() > totalNeeded) {
|
||||||
recvBuffer = recvBuffer.substr(totalNeeded);
|
recvBuffer = recvBuffer.substr(totalNeeded);
|
||||||
} else {
|
} else {
|
||||||
@ -403,6 +593,8 @@ void CalibreWirelessActivity::sendJsonResponse(const OpCode opcode, const std::s
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CalibreWirelessActivity::handleCommand(const OpCode opcode, const std::string& data) {
|
void CalibreWirelessActivity::handleCommand(const OpCode opcode, const std::string& data) {
|
||||||
|
Serial.printf("[%lu] [CAL] Received opcode: %d, data size: %zu\n", millis(), opcode, data.size());
|
||||||
|
|
||||||
switch (opcode) {
|
switch (opcode) {
|
||||||
case OpCode::GET_INITIALIZATION_INFO:
|
case OpCode::GET_INITIALIZATION_INFO:
|
||||||
handleGetInitializationInfo(data);
|
handleGetInitializationInfo(data);
|
||||||
@ -475,8 +667,9 @@ void CalibreWirelessActivity::handleGetInitializationInfo(const std::string& dat
|
|||||||
// ccVersionNumber: Calibre Companion protocol version. 212 matches CC 5.4.20+.
|
// ccVersionNumber: Calibre Companion protocol version. 212 matches CC 5.4.20+.
|
||||||
// Using a known version ensures compatibility with Calibre's feature detection.
|
// Using a known version ensures compatibility with Calibre's feature detection.
|
||||||
response += "\"ccVersionNumber\":212,";
|
response += "\"ccVersionNumber\":212,";
|
||||||
// coverHeight: Max cover image height. We don't process covers, so this is informational only.
|
// coverHeight: Max cover image height. Set to 0 to prevent Calibre from embedding
|
||||||
response += "\"coverHeight\":800,";
|
// large base64-encoded covers in SEND_BOOK metadata, which would bloat the JSON.
|
||||||
|
response += "\"coverHeight\":0,";
|
||||||
response += "\"deviceKind\":\"CrossPoint\",";
|
response += "\"deviceKind\":\"CrossPoint\",";
|
||||||
response += "\"deviceName\":\"CrossPoint\",";
|
response += "\"deviceName\":\"CrossPoint\",";
|
||||||
response += "\"extensionPathLengths\":{\"epub\":37},";
|
response += "\"extensionPathLengths\":{\"epub\":37},";
|
||||||
@ -522,14 +715,19 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) {
|
|||||||
// Extract "lpath" field - format: "lpath": "value"
|
// Extract "lpath" field - format: "lpath": "value"
|
||||||
std::string lpath;
|
std::string lpath;
|
||||||
size_t lpathPos = data.find("\"lpath\"");
|
size_t lpathPos = data.find("\"lpath\"");
|
||||||
if (lpathPos != std::string::npos) {
|
if (lpathPos != std::string::npos && lpathPos + 7 < data.size()) {
|
||||||
size_t colonPos = data.find(':', lpathPos + 7);
|
size_t colonPos = data.find(':', lpathPos + 7);
|
||||||
if (colonPos != std::string::npos) {
|
if (colonPos != std::string::npos && colonPos + 1 < data.size()) {
|
||||||
size_t quoteStart = data.find('"', colonPos + 1);
|
size_t quoteStart = data.find('"', colonPos + 1);
|
||||||
if (quoteStart != std::string::npos) {
|
if (quoteStart != std::string::npos && quoteStart + 1 < data.size()) {
|
||||||
size_t quoteEnd = data.find('"', quoteStart + 1);
|
size_t quoteEnd = data.find('"', quoteStart + 1);
|
||||||
if (quoteEnd != std::string::npos) {
|
if (quoteEnd != std::string::npos && quoteEnd > quoteStart + 1) {
|
||||||
lpath = data.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
|
// Safe bounds check before substr
|
||||||
|
size_t start = quoteStart + 1;
|
||||||
|
size_t len = quoteEnd - quoteStart - 1;
|
||||||
|
if (start < data.size() && start + len <= data.size()) {
|
||||||
|
lpath = data.substr(start, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -539,6 +737,9 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) {
|
|||||||
// The metadata contains nested "length" fields (e.g., cover image length)
|
// The metadata contains nested "length" fields (e.g., cover image length)
|
||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
|
const char* lengthKey = "\"length\"";
|
||||||
|
const size_t keyLen = 8;
|
||||||
|
|
||||||
for (size_t i = 0; i < data.size(); i++) {
|
for (size_t i = 0; i < data.size(); i++) {
|
||||||
char c = data[i];
|
char c = data[i];
|
||||||
if (c == '{' || c == '[') {
|
if (c == '{' || c == '[') {
|
||||||
@ -546,22 +747,35 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) {
|
|||||||
} else if (c == '}' || c == ']') {
|
} else if (c == '}' || c == ']') {
|
||||||
depth--;
|
depth--;
|
||||||
} else if (depth == 1 && c == '"') {
|
} else if (depth == 1 && c == '"') {
|
||||||
// At top level, check if this is "length"
|
// At top level, check if this is "length" by comparing directly
|
||||||
if (i + 9 < data.size() && data.substr(i, 8) == "\"length\"") {
|
if (i + keyLen <= data.size()) {
|
||||||
// Found top-level "length" - extract the number after ':'
|
bool match = true;
|
||||||
size_t colonPos = data.find(':', i + 8);
|
for (size_t j = 0; j < keyLen && match; j++) {
|
||||||
if (colonPos != std::string::npos) {
|
if (data[i + j] != lengthKey[j]) {
|
||||||
size_t numStart = colonPos + 1;
|
match = false;
|
||||||
while (numStart < data.size() && (data[numStart] == ' ' || data[numStart] == '\t')) {
|
|
||||||
numStart++;
|
|
||||||
}
|
}
|
||||||
size_t numEnd = numStart;
|
}
|
||||||
while (numEnd < data.size() && data[numEnd] >= '0' && data[numEnd] <= '9') {
|
if (match) {
|
||||||
numEnd++;
|
// Found top-level "length" - extract the number after ':'
|
||||||
|
size_t colonPos = i + keyLen;
|
||||||
|
while (colonPos < data.size() && data[colonPos] != ':') {
|
||||||
|
colonPos++;
|
||||||
}
|
}
|
||||||
if (numEnd > numStart) {
|
if (colonPos < data.size()) {
|
||||||
length = std::stoul(data.substr(numStart, numEnd - numStart));
|
size_t numStart = colonPos + 1;
|
||||||
break;
|
while (numStart < data.size() && (data[numStart] == ' ' || data[numStart] == '\t')) {
|
||||||
|
numStart++;
|
||||||
|
}
|
||||||
|
// Parse number safely without exceptions
|
||||||
|
size_t parsedLen = 0;
|
||||||
|
while (numStart < data.size() && data[numStart] >= '0' && data[numStart] <= '9') {
|
||||||
|
parsedLen = parsedLen * 10 + (data[numStart] - '0');
|
||||||
|
numStart++;
|
||||||
|
}
|
||||||
|
if (parsedLen > 0) {
|
||||||
|
length = parsedLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -588,6 +802,8 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) {
|
|||||||
currentFileSize = length;
|
currentFileSize = length;
|
||||||
bytesReceived = 0;
|
bytesReceived = 0;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [CAL] SEND_BOOK: lpath='%s', length=%zu\n", millis(), lpath.c_str(), length);
|
||||||
|
|
||||||
setState(WirelessState::RECEIVING);
|
setState(WirelessState::RECEIVING);
|
||||||
setStatus("Receiving: " + filename);
|
setStatus("Receiving: " + filename);
|
||||||
|
|
||||||
@ -640,39 +856,49 @@ void CalibreWirelessActivity::handleNoop(const std::string& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CalibreWirelessActivity::receiveBinaryData() {
|
void CalibreWirelessActivity::receiveBinaryData() {
|
||||||
const int available = tcpClient.available();
|
// Read all available data in a loop to prevent TCP backpressure
|
||||||
if (available == 0) {
|
// This is important because Calibre sends data continuously
|
||||||
// Check if connection is still alive
|
while (binaryBytesRemaining > 0) {
|
||||||
if (!tcpClient.connected()) {
|
const int available = tcpClient.available();
|
||||||
currentFile.close();
|
if (available == 0) {
|
||||||
inBinaryMode = false;
|
// Check if connection is still alive
|
||||||
setError("Transfer interrupted");
|
if (!tcpClient.connected()) {
|
||||||
|
Serial.printf("[%lu] [CAL] Connection lost during binary transfer. Received %zu/%zu bytes\n",
|
||||||
|
millis(), bytesReceived, currentFileSize);
|
||||||
|
currentFile.close();
|
||||||
|
inBinaryMode = false;
|
||||||
|
setError("Transfer interrupted");
|
||||||
|
}
|
||||||
|
return; // No data available right now, will continue next iteration
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t buffer[1024];
|
uint8_t buffer[4096]; // Larger buffer for faster transfer
|
||||||
const size_t toRead = std::min(sizeof(buffer), binaryBytesRemaining);
|
const size_t toRead = std::min({sizeof(buffer), binaryBytesRemaining, static_cast<size_t>(available)});
|
||||||
const size_t bytesRead = tcpClient.read(buffer, toRead);
|
const size_t bytesRead = tcpClient.read(buffer, toRead);
|
||||||
|
|
||||||
|
if (bytesRead == 0) {
|
||||||
|
break; // No more data to read right now
|
||||||
|
}
|
||||||
|
|
||||||
if (bytesRead > 0) {
|
|
||||||
currentFile.write(buffer, bytesRead);
|
currentFile.write(buffer, bytesRead);
|
||||||
bytesReceived += bytesRead;
|
bytesReceived += bytesRead;
|
||||||
binaryBytesRemaining -= bytesRead;
|
binaryBytesRemaining -= bytesRead;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (binaryBytesRemaining == 0) {
|
if (binaryBytesRemaining == 0) {
|
||||||
// Transfer complete
|
// Transfer complete - switch back to JSON mode
|
||||||
currentFile.flush();
|
// Note: Do NOT send OK here. KOReader doesn't, and sending an extra OK
|
||||||
currentFile.close();
|
// could be misinterpreted as a response to SEND_BOOK_METADATA before
|
||||||
inBinaryMode = false;
|
// we've received it, causing protocol desync.
|
||||||
|
currentFile.flush();
|
||||||
|
currentFile.close();
|
||||||
|
inBinaryMode = false;
|
||||||
|
|
||||||
setState(WirelessState::WAITING);
|
Serial.printf("[%lu] [CAL] Binary transfer complete: %zu bytes received\n", millis(), bytesReceived);
|
||||||
setStatus("Received: " + currentFilename + "\nWaiting for more...");
|
|
||||||
|
|
||||||
// Send OK to acknowledge completion
|
setState(WirelessState::WAITING);
|
||||||
sendJsonResponse(OpCode::OK, "{}");
|
setStatus("Received: " + currentFilename + "\nWaiting for more...");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,13 @@ class CalibreWirelessActivity final : public Activity {
|
|||||||
FsFile currentFile;
|
FsFile currentFile;
|
||||||
std::string recvBuffer; // Buffer for incoming data (like KOReader)
|
std::string recvBuffer; // Buffer for incoming data (like KOReader)
|
||||||
|
|
||||||
|
// Large message skip state - for streaming past oversized JSON (e.g., large covers)
|
||||||
|
bool inSkipMode = false;
|
||||||
|
size_t skipBytesRemaining = 0;
|
||||||
|
int skipOpcode = -1; // Opcode of message being skipped
|
||||||
|
std::string skipExtractedLpath;
|
||||||
|
size_t skipExtractedLength = 0;
|
||||||
|
|
||||||
static void displayTaskTrampoline(void* param);
|
static void displayTaskTrampoline(void* param);
|
||||||
static void networkTaskTrampoline(void* param);
|
static void networkTaskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user