mirror of
https://github.com/Senape3000/EvilCrowRF-V2.git
synced 2026-03-30 13:45:39 +00:00
Introduce the ProtoPirate automotive key‑fob decoder and integrate it end‑to‑end across firmware and mobile app. Changes include: new binary message IDs for ProtoPirate (MSG_PP_*), config enable flag and firmware patch bump to 1.1.2, and a new ProtoPirateCommands BLE/serial handler that implements start/stop, history, file browsing (FATFS), emulate (TX) and save/list captures. Firmware updates: added ProtoPirate module headers/implementation, history support, FileCommands improvements (path handling, file streaming size guard, create /DATA/PROTOPIRATE directory), and safer ConfigManager aggregate initialization compatible with older GCC. Mobile app updates: new ProtoPirate UI screen, model (ProtoPirateResult), BLE provider wiring (reset state, imports) and localization strings (EN/RU) and generated localization stubs. Misc: increased BLE chunk timeouts and less aggressive chunk buffer cleanup, plus a helper script tools/generate_test_sub.py.
1610 lines
65 KiB
C++
1610 lines
65 KiB
C++
#ifndef FileCommands_h
|
|
#define FileCommands_h
|
|
|
|
#include "StringBuffer.h"
|
|
#include "core/ble/CommandHandler.h"
|
|
#include "core/ble/ClientsManager.h"
|
|
#include "StringHelpers.h"
|
|
#include "BinaryMessages.h"
|
|
#include "core/ble/BleAdapter.h"
|
|
#include "SD.h"
|
|
#include <LittleFS.h>
|
|
#include "Arduino.h"
|
|
#include <cstring> // For strrchr
|
|
#include <vector> // For std::vector
|
|
#include "ff.h" // FATFS low-level API for fast directory reading
|
|
|
|
// Forward declarations
|
|
extern ClientsManager& clients;
|
|
|
|
/**
|
|
* File commands using static buffers
|
|
* instead of dynamic strings to save memory on microcontrollers
|
|
*/
|
|
class FileCommands {
|
|
public:
|
|
static void registerCommands(CommandHandler& handler) {
|
|
handler.registerCommand(0x05, handleGetFilesList);
|
|
handler.registerCommand(0x09, handleLoadFileData);
|
|
handler.registerCommand(0x0B, handleRemoveFile);
|
|
handler.registerCommand(0x0C, handleRenameFile);
|
|
handler.registerCommand(0x0A, handleCreateDirectory);
|
|
// 0x0D (upload) is handled specially in BleAdapter::handleUploadChunk, not via CommandHandler
|
|
handler.registerCommand(0x0E, handleCopyFile);
|
|
handler.registerCommand(0x0F, handleMoveFile);
|
|
handler.registerCommand(0x10, handleSaveToSignalsWithName);
|
|
handler.registerCommand(0x14, handleGetDirectoryTree); // Changed from 0x12 to avoid conflict with startJam
|
|
handler.registerCommand(0x18, handleFormatSDCard);
|
|
}
|
|
|
|
private:
|
|
// Static buffers to avoid dynamic allocations
|
|
static JsonBuffer jsonBuffer;
|
|
static PathBuffer pathBuffer;
|
|
static LogBuffer logBuffer;
|
|
|
|
// Helper functions for path operations
|
|
|
|
/**
|
|
* Returns the appropriate filesystem for the given pathType.
|
|
* pathType 0-3 and 5 use SD card, pathType 4 uses LittleFS (internal flash).
|
|
*/
|
|
static fs::FS& getFS(uint8_t pathType) {
|
|
if (pathType == 4) return LittleFS;
|
|
return SD;
|
|
}
|
|
|
|
/**
|
|
* Buffered file copy between two open File handles.
|
|
* Uses a 512-byte stack buffer for efficient block transfer
|
|
* instead of byte-by-byte read/write.
|
|
* @return true on success, false on write error
|
|
*/
|
|
static bool bufferedFileCopy(File& src, File& dst) {
|
|
uint8_t buf[512];
|
|
while (src.available()) {
|
|
size_t toRead = std::min((size_t)src.available(), sizeof(buf));
|
|
size_t bytesRead = src.read(buf, toRead);
|
|
if (bytesRead == 0) break;
|
|
size_t written = dst.write(buf, bytesRead);
|
|
if (written != bytesRead) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Builds base path from pathType
|
|
* @param pathType 0=RECORDS, 1=SIGNALS, 2=PRESETS, 3=TEMP, 4=INTERNAL (LittleFS root), 5=SD root
|
|
* @param buffer buffer to receive result
|
|
*/
|
|
static void buildBasePath(uint8_t pathType, PathBuffer& buffer) {
|
|
buffer.clear();
|
|
if (pathType == 4 || pathType == 5) {
|
|
// Root-based storage (LittleFS for 4, SD root for 5)
|
|
return;
|
|
}
|
|
buffer.append("/DATA/");
|
|
switch (pathType) {
|
|
case 0: buffer.append("RECORDS"); break;
|
|
case 1: buffer.append("SIGNALS"); break;
|
|
case 2: buffer.append("PRESETS"); break;
|
|
case 3: buffer.append("TEMP"); break;
|
|
default: buffer.append("RECORDS"); break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds full path from pathType and relative path
|
|
* @param pathType 0=RECORDS, 1=SIGNALS, 2=PRESETS, 3=TEMP, 4=INTERNAL, 5=SD root
|
|
* @param relativePath relative path (may be empty or "/")
|
|
* @param pathLen length of relative path
|
|
* @param buffer buffer to receive result
|
|
*/
|
|
static void buildFullPath(uint8_t pathType, const char* relativePath, size_t pathLen, PathBuffer& buffer) {
|
|
buildBasePath(pathType, buffer);
|
|
|
|
// For root-based storages (pathType 4/5), base path is empty so start with "/"
|
|
if ((pathType == 4 || pathType == 5) && buffer.size() == 0) {
|
|
buffer.append("/");
|
|
}
|
|
|
|
// Add path if not root
|
|
if (pathLen > 0) {
|
|
// Check whether the path is root
|
|
if (pathLen != 1 || relativePath[0] != '/') {
|
|
// Strip leading slash from relativePath to avoid double slash
|
|
const char* p = relativePath;
|
|
size_t pLen = pathLen;
|
|
if (p[0] == '/') {
|
|
p++;
|
|
pLen--;
|
|
}
|
|
|
|
// Add separator only if buffer doesn't already end with '/'
|
|
size_t bufSz = buffer.size();
|
|
if (bufSz > 0 && buffer.c_str()[bufSz - 1] != '/') {
|
|
buffer.append("/");
|
|
}
|
|
|
|
if (pLen > 0) {
|
|
buffer.append(p, pLen);
|
|
}
|
|
} else {
|
|
// Root path "/" - ensure trailing slash for directory
|
|
size_t bufSz = buffer.size();
|
|
if (bufSz == 0 || buffer.c_str()[bufSz - 1] != '/') {
|
|
buffer.append("/");
|
|
}
|
|
}
|
|
} else {
|
|
// Empty path - ensure trailing slash for root directory
|
|
size_t bufSz = buffer.size();
|
|
if (bufSz == 0 || buffer.c_str()[bufSz - 1] != '/') {
|
|
buffer.append("/");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts filename from full path
|
|
* @param fullPath full path
|
|
* @param filename buffer to receive filename
|
|
*/
|
|
static void extractFilename(const char* fullPath, PathBuffer& filename) {
|
|
filename.clear();
|
|
const char* lastSlash = strrchr(fullPath, '/');
|
|
if (lastSlash) {
|
|
filename.append(lastSlash + 1);
|
|
} else {
|
|
filename.append(fullPath);
|
|
}
|
|
}
|
|
|
|
// Binary tree builder
|
|
static void buildDirectoryTreeBinaryRecursive(const char* path, uint8_t* buffer, size_t& offset, uint16_t& count, size_t maxBufferSize = 1024) {
|
|
// Use FATFS low-level API for O(n) directory traversal
|
|
char fatfsPath[256];
|
|
snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", path);
|
|
|
|
FF_DIR fatDir;
|
|
FILINFO fno;
|
|
FRESULT res = f_opendir(&fatDir, fatfsPath);
|
|
if (res != FR_OK) {
|
|
// Try without /sd prefix
|
|
res = f_opendir(&fatDir, path);
|
|
if (res != FR_OK) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint16_t entriesProcessed = 0;
|
|
while (true) {
|
|
res = f_readdir(&fatDir, &fno);
|
|
if (res != FR_OK || fno.fname[0] == 0) {
|
|
// No more entries
|
|
break;
|
|
}
|
|
|
|
// Skip . and ..
|
|
if (fno.fname[0] == '.' && (fno.fname[1] == '\0' || (fno.fname[1] == '.' && fno.fname[2] == '\0'))) {
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a directory
|
|
bool isDir = (fno.fname[0] != 0 && (fno.fattrib & AM_DIR) != 0);
|
|
|
|
if (isDir) {
|
|
// Build full path
|
|
char dirPath[256];
|
|
if (strcmp(path, "/") == 0) {
|
|
snprintf(dirPath, sizeof(dirPath), "/%s", fno.fname);
|
|
} else {
|
|
snprintf(dirPath, sizeof(dirPath), "%s/%s", path, fno.fname);
|
|
}
|
|
|
|
uint8_t pathLen = (uint8_t)strlen(dirPath);
|
|
|
|
// Check buffer space
|
|
if (offset + 1 + pathLen >= maxBufferSize) {
|
|
// Buffer full, cannot add more
|
|
break;
|
|
}
|
|
|
|
buffer[offset++] = pathLen;
|
|
memcpy(buffer + offset, dirPath, pathLen);
|
|
offset += pathLen;
|
|
count++;
|
|
|
|
// Recurse into subdirectory
|
|
buildDirectoryTreeBinaryRecursive(dirPath, buffer, offset, count, maxBufferSize);
|
|
}
|
|
|
|
entriesProcessed++;
|
|
// Yield every 10 entries to prevent watchdog timeout
|
|
if (entriesProcessed % 10 == 0) {
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
}
|
|
}
|
|
|
|
f_closedir(&fatDir);
|
|
}
|
|
|
|
public:
|
|
// Get files list - STREAMING BINARY PROTOCOL (no JSON!)
|
|
// Sends multiple messages for large directories to minimize memory usage.
|
|
//
|
|
// Response format (each message):
|
|
// [0xA1][pathLen:1][path:pathLen][flags:1][totalFiles:2][fileCount:1][files...]
|
|
//
|
|
// flags byte:
|
|
// bit 0 (0x01): hasMore - 1=more messages coming, 0=last message
|
|
// bit 7 (0x80): error - if set, bits 1-6 contain error code, fileCount=0
|
|
//
|
|
// totalFiles: total number of files in directory (for progress calculation)
|
|
// fileCount: number of files in THIS message (1 byte, max 255)
|
|
//
|
|
// For each file:
|
|
// [nameLen:1][name:nameLen][fileFlags:1]
|
|
// If file (fileFlags & 0x01 == 0):
|
|
// [size:4][date:4] (little-endian)
|
|
//
|
|
// Error codes (when flags & 0x80):
|
|
// 1 = insufficient memory
|
|
// 2 = failed to create directory
|
|
// 3 = failed to open directory
|
|
// 4 = path is not a directory
|
|
// 5 = unknown error
|
|
static bool handleGetFilesList(const uint8_t* data, size_t len) {
|
|
// CRITICAL: Prevent concurrent execution
|
|
static bool isProcessing = false;
|
|
if (isProcessing) {
|
|
ESP_LOGW("FileCommands", "handleGetFilesList already in progress");
|
|
return false;
|
|
}
|
|
|
|
isProcessing = true;
|
|
bool success = false;
|
|
|
|
if (len < 2) {
|
|
isProcessing = false;
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathLength = data[0];
|
|
uint8_t pathType = data[1];
|
|
|
|
if (len < 2 + pathLength) {
|
|
isProcessing = false;
|
|
return false;
|
|
}
|
|
|
|
// Build full path
|
|
const char* path = (pathLength > 0) ? reinterpret_cast<const char*>(data + 2) : nullptr;
|
|
buildFullPath(pathType, path, pathLength, pathBuffer);
|
|
|
|
// Check memory
|
|
if (ESP.getFreeHeap() < 3000) {
|
|
sendBinaryFileListError(1);
|
|
isProcessing = false;
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// Prepare directory path without trailing slash
|
|
static PathBuffer dirPathWithoutSlash;
|
|
dirPathWithoutSlash.clear();
|
|
const char* pathStr = pathBuffer.c_str();
|
|
size_t pathStrLen = strlen(pathStr);
|
|
if (pathStrLen > 0 && pathStr[pathStrLen - 1] == '/') {
|
|
dirPathWithoutSlash.append(pathStr, pathStrLen - 1);
|
|
} else {
|
|
dirPathWithoutSlash.append(pathStr, pathStrLen);
|
|
}
|
|
|
|
// Create directory if it doesn't exist (only dedicated SD folders 0..3)
|
|
if (pathType <= 3 && !SD.exists(dirPathWithoutSlash.c_str())) {
|
|
const char* dirPathStr = dirPathWithoutSlash.c_str();
|
|
size_t dirPathLen = strlen(dirPathStr);
|
|
static PathBuffer currentPath;
|
|
|
|
for (size_t i = 1; i < dirPathLen; i++) {
|
|
if (dirPathStr[i] == '/') {
|
|
currentPath.clear();
|
|
currentPath.append(dirPathStr, i);
|
|
if (!SD.exists(currentPath.c_str())) {
|
|
SD.mkdir(currentPath.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!SD.mkdir(dirPathWithoutSlash.c_str())) {
|
|
sendBinaryFileListError(2);
|
|
isProcessing = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// --- LittleFS listing (pathType 4) uses Arduino File API ---
|
|
if (pathType == 4) {
|
|
uint32_t streamStartTime = millis();
|
|
const char* listPath = (dirPathWithoutSlash.size() > 0)
|
|
? dirPathWithoutSlash.c_str() : "/";
|
|
File root = LittleFS.open(listPath);
|
|
if (!root || !root.isDirectory()) {
|
|
if (root) root.close();
|
|
// Try root "/" if requested path fails
|
|
root = LittleFS.open("/");
|
|
if (!root) {
|
|
sendBinaryFileListError(3);
|
|
isProcessing = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Collect all entries (LittleFS typically has <10 files)
|
|
const size_t BUFFER_SIZE = 500;
|
|
static uint8_t binaryBuffer[BUFFER_SIZE];
|
|
size_t pathLen = strlen(pathBuffer.c_str());
|
|
uint16_t totalFilesSent = 0;
|
|
|
|
size_t bufferOffset = 0;
|
|
binaryBuffer[bufferOffset++] = MSG_FILE_LIST;
|
|
binaryBuffer[bufferOffset++] = (uint8_t)pathLen;
|
|
memcpy(binaryBuffer + bufferOffset, pathBuffer.c_str(), pathLen);
|
|
bufferOffset += pathLen;
|
|
|
|
size_t flagsOffset = bufferOffset++;
|
|
size_t totalFilesOffset = bufferOffset;
|
|
bufferOffset += 2;
|
|
size_t fileCountOffset = bufferOffset++;
|
|
|
|
File child = root.openNextFile();
|
|
while (child) {
|
|
const char* name = child.name();
|
|
// Strip leading '/' if present
|
|
if (name[0] == '/') name++;
|
|
uint8_t nameLen = strlen(name);
|
|
if (nameLen > 255) nameLen = 255;
|
|
bool isDir = child.isDirectory();
|
|
uint32_t fileSize = isDir ? 0 : child.size();
|
|
|
|
size_t entrySize = 1 + nameLen + 1 + (isDir ? 0 : 8);
|
|
if (bufferOffset + entrySize >= BUFFER_SIZE - 4) break; // safety margin
|
|
|
|
binaryBuffer[bufferOffset++] = nameLen;
|
|
memcpy(binaryBuffer + bufferOffset, name, nameLen);
|
|
bufferOffset += nameLen;
|
|
binaryBuffer[bufferOffset++] = isDir ? 0x01 : 0x00;
|
|
|
|
if (!isDir) {
|
|
binaryBuffer[bufferOffset++] = fileSize & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 8) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 16) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 24) & 0xFF;
|
|
// No timestamp available on LittleFS, send 0
|
|
binaryBuffer[bufferOffset++] = 0;
|
|
binaryBuffer[bufferOffset++] = 0;
|
|
binaryBuffer[bufferOffset++] = 0;
|
|
binaryBuffer[bufferOffset++] = 0;
|
|
}
|
|
|
|
totalFilesSent++;
|
|
child = root.openNextFile();
|
|
}
|
|
root.close();
|
|
|
|
binaryBuffer[flagsOffset] = 0x00; // No more messages
|
|
binaryBuffer[fileCountOffset] = (uint8_t)totalFilesSent;
|
|
binaryBuffer[totalFilesOffset] = totalFilesSent & 0xFF;
|
|
binaryBuffer[totalFilesOffset + 1] = (totalFilesSent >> 8) & 0xFF;
|
|
|
|
clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset);
|
|
|
|
uint32_t totalTime = millis() - streamStartTime;
|
|
ESP_LOGD("FileCommands", "LittleFS list: %d files, %lu ms", totalFilesSent, totalTime);
|
|
isProcessing = false;
|
|
return true;
|
|
}
|
|
|
|
// Use FATFS directly for O(n) directory reading instead of O(n²)
|
|
// Arduino's openNextFile() rescans from beginning each time
|
|
uint32_t streamStartTime = millis();
|
|
|
|
// Build FATFS path (needs /sd prefix for ESP32)
|
|
char fatfsPath[270];
|
|
if (dirPathWithoutSlash.size() > 0) {
|
|
snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", dirPathWithoutSlash.c_str());
|
|
} else {
|
|
snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", pathBuffer.c_str());
|
|
}
|
|
|
|
FF_DIR fatDir;
|
|
FILINFO fno;
|
|
FRESULT res = f_opendir(&fatDir, fatfsPath);
|
|
if (res != FR_OK) {
|
|
// Try without /sd prefix
|
|
res = f_opendir(&fatDir, dirPathWithoutSlash.c_str());
|
|
if (res != FR_OK) {
|
|
sendBinaryFileListError(3);
|
|
isProcessing = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// STREAMING: Use buffer that fits in single BLE chunk (MAX_CHUNK_SIZE = 500)
|
|
// BLE notify limit is 509 bytes, so 500 bytes data + 7 header + 1 checksum = 508 bytes total
|
|
const size_t BUFFER_SIZE = 500;
|
|
const size_t MAX_FILES_PER_MESSAGE = 50; // More files per message since reading is faster now
|
|
static uint8_t binaryBuffer[BUFFER_SIZE];
|
|
|
|
size_t pathLen = strlen(pathBuffer.c_str());
|
|
|
|
uint16_t totalFilesSent = 0;
|
|
bool hasMoreFiles = true;
|
|
bool lowMemory = false;
|
|
uint8_t messagesSent = 0;
|
|
|
|
// Pending file info (when buffer is full, save file for next iteration)
|
|
static char pendingFilename[256];
|
|
static bool hasPendingFile = false;
|
|
static bool pendingIsDir = false;
|
|
static uint32_t pendingFileSize = 0;
|
|
static uint32_t pendingFileDate = 0;
|
|
|
|
hasPendingFile = false;
|
|
|
|
while (hasMoreFiles && !lowMemory) {
|
|
uint32_t msgStartTime = millis();
|
|
|
|
// Build message header
|
|
size_t bufferOffset = 0;
|
|
binaryBuffer[bufferOffset++] = MSG_FILE_LIST; // 0xA1
|
|
binaryBuffer[bufferOffset++] = (uint8_t)pathLen;
|
|
memcpy(binaryBuffer + bufferOffset, pathBuffer.c_str(), pathLen);
|
|
bufferOffset += pathLen;
|
|
|
|
size_t flagsOffset = bufferOffset++;
|
|
size_t totalFilesOffset = bufferOffset;
|
|
bufferOffset += 2;
|
|
size_t fileCountOffset = bufferOffset++;
|
|
|
|
uint8_t filesInThisMessage = 0;
|
|
|
|
// First, add pending file from previous iteration
|
|
if (hasPendingFile) {
|
|
uint8_t nameLen = strlen(pendingFilename);
|
|
size_t entrySize = 1 + nameLen + 1 + (pendingIsDir ? 0 : 8);
|
|
|
|
binaryBuffer[bufferOffset++] = nameLen;
|
|
memcpy(binaryBuffer + bufferOffset, pendingFilename, nameLen);
|
|
bufferOffset += nameLen;
|
|
binaryBuffer[bufferOffset++] = pendingIsDir ? 0x01 : 0x00;
|
|
|
|
if (!pendingIsDir) {
|
|
binaryBuffer[bufferOffset++] = pendingFileSize & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileSize >> 8) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileSize >> 16) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileSize >> 24) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = pendingFileDate & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileDate >> 8) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileDate >> 16) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (pendingFileDate >> 24) & 0xFF;
|
|
}
|
|
|
|
filesInThisMessage++;
|
|
totalFilesSent++;
|
|
hasPendingFile = false;
|
|
}
|
|
|
|
// Read directory entries using FATFS - O(n) complexity!
|
|
while (filesInThisMessage < MAX_FILES_PER_MESSAGE) {
|
|
res = f_readdir(&fatDir, &fno);
|
|
if (res != FR_OK || fno.fname[0] == 0) {
|
|
// No more files
|
|
break;
|
|
}
|
|
|
|
// Skip . and ..
|
|
if (fno.fname[0] == '.') continue;
|
|
|
|
// Check memory
|
|
if (ESP.getFreeHeap() < 2000) {
|
|
lowMemory = true;
|
|
break;
|
|
}
|
|
|
|
const char* filename = fno.fname;
|
|
uint8_t nameLen = strlen(filename);
|
|
if (nameLen > 255) nameLen = 255;
|
|
|
|
bool isDir = (fno.fattrib & AM_DIR) != 0;
|
|
uint32_t fileSize = isDir ? 0 : fno.fsize;
|
|
|
|
// Convert FAT date/time to Unix timestamp
|
|
// FAT date: bits 15-9=year-1980, 8-5=month, 4-0=day
|
|
// FAT time: bits 15-11=hour, 10-5=minute, 4-0=second/2
|
|
uint16_t fatDate = fno.fdate;
|
|
uint16_t fatTime = fno.ftime;
|
|
|
|
// Manual conversion to Unix timestamp (seconds since 1970)
|
|
int year = ((fatDate >> 9) & 0x7F) + 1980;
|
|
int month = ((fatDate >> 5) & 0x0F);
|
|
int day = fatDate & 0x1F;
|
|
int hour = (fatTime >> 11) & 0x1F;
|
|
int minute = (fatTime >> 5) & 0x3F;
|
|
int second = (fatTime & 0x1F) * 2;
|
|
|
|
// Days from 1970 to year
|
|
uint32_t days = 0;
|
|
for (int y = 1970; y < year; y++) {
|
|
days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365;
|
|
}
|
|
// Days in current year
|
|
static const int monthDays[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
|
|
if (month >= 1 && month <= 12) {
|
|
days += monthDays[month - 1];
|
|
// Leap year adjustment
|
|
if (month > 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) {
|
|
days++;
|
|
}
|
|
}
|
|
days += (day > 0 ? day - 1 : 0);
|
|
|
|
uint32_t fileDate = days * 86400 + hour * 3600 + minute * 60 + second;
|
|
|
|
// Calculate entry size
|
|
size_t entrySize = 1 + nameLen + 1 + (isDir ? 0 : 8);
|
|
|
|
// Check if entry fits in remaining buffer space
|
|
if (bufferOffset + entrySize >= BUFFER_SIZE - 16) {
|
|
// Buffer full - save this file for next iteration
|
|
strncpy(pendingFilename, filename, sizeof(pendingFilename) - 1);
|
|
pendingFilename[sizeof(pendingFilename) - 1] = 0;
|
|
pendingIsDir = isDir;
|
|
pendingFileSize = fileSize;
|
|
pendingFileDate = fileDate;
|
|
hasPendingFile = true;
|
|
break;
|
|
}
|
|
|
|
// Add file entry to buffer
|
|
binaryBuffer[bufferOffset++] = nameLen;
|
|
memcpy(binaryBuffer + bufferOffset, filename, nameLen);
|
|
bufferOffset += nameLen;
|
|
binaryBuffer[bufferOffset++] = isDir ? 0x01 : 0x00;
|
|
|
|
if (!isDir) {
|
|
binaryBuffer[bufferOffset++] = fileSize & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 8) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 16) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileSize >> 24) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = fileDate & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileDate >> 8) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileDate >> 16) & 0xFF;
|
|
binaryBuffer[bufferOffset++] = (fileDate >> 24) & 0xFF;
|
|
}
|
|
|
|
filesInThisMessage++;
|
|
totalFilesSent++;
|
|
|
|
// Yield every 20 files to prevent watchdog (faster now, so less frequent)
|
|
if (filesInThisMessage % 20 == 0) {
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
}
|
|
}
|
|
uint32_t readTime = millis() - msgStartTime;
|
|
|
|
// Check if we read all files (no pending and last f_readdir returned empty)
|
|
if (!hasPendingFile && (res != FR_OK || fno.fname[0] == 0)) {
|
|
hasMoreFiles = false;
|
|
}
|
|
|
|
// Update flags and fileCount
|
|
binaryBuffer[flagsOffset] = hasMoreFiles ? 0x01 : 0x00;
|
|
binaryBuffer[fileCountOffset] = filesInThisMessage;
|
|
|
|
// Set totalFiles: 0xFFFF if more coming, actual count if this is last message
|
|
if (hasMoreFiles) {
|
|
binaryBuffer[totalFilesOffset] = 0xFF;
|
|
binaryBuffer[totalFilesOffset + 1] = 0xFF;
|
|
} else {
|
|
binaryBuffer[totalFilesOffset] = totalFilesSent & 0xFF;
|
|
binaryBuffer[totalFilesOffset + 1] = (totalFilesSent >> 8) & 0xFF;
|
|
}
|
|
|
|
// Send this message
|
|
if (filesInThisMessage > 0 || !hasMoreFiles) {
|
|
clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset);
|
|
messagesSent++;
|
|
|
|
// Log only in debug mode to save resources in production
|
|
ESP_LOGD("FileCommands", "Msg %d: %d files, read=%lums",
|
|
messagesSent, filesInThisMessage, readTime);
|
|
|
|
// Small delay to allow mobile app to process chunks and update UI
|
|
// Reduced from 150ms to 50ms - chunk processing is fast, and we have
|
|
// improved chunk buffer cleanup on mobile side. BLE notifications are queued,
|
|
// so this prevents overwhelming the receiver while still being responsive
|
|
// In production, reduce delay slightly for better performance
|
|
vTaskDelay(pdMS_TO_TICKS(30));
|
|
}
|
|
|
|
// Safety: prevent infinite loop
|
|
if (filesInThisMessage == 0 && !hasPendingFile) {
|
|
hasMoreFiles = false;
|
|
}
|
|
}
|
|
|
|
f_closedir(&fatDir);
|
|
|
|
uint32_t totalTime = millis() - streamStartTime;
|
|
// Log only in debug mode to save resources in production
|
|
ESP_LOGD("FileCommands", "File list complete: %d files, %d msgs, %lu ms total",
|
|
totalFilesSent, messagesSent, totalTime);
|
|
success = true;
|
|
|
|
} catch (...) {
|
|
sendBinaryFileListError(5);
|
|
}
|
|
|
|
isProcessing = false;
|
|
return success;
|
|
}
|
|
|
|
// Send binary error response for file list
|
|
// flags byte has bit 7 set (0x80) plus error code in bits 0-6
|
|
static void sendBinaryFileListError(uint8_t errorCode) {
|
|
size_t pathLen = strlen(pathBuffer.c_str());
|
|
static uint8_t errorBuffer[264];
|
|
size_t offset = 0;
|
|
|
|
errorBuffer[offset++] = MSG_FILE_LIST; // 0xA1
|
|
errorBuffer[offset++] = (uint8_t)pathLen;
|
|
memcpy(errorBuffer + offset, pathBuffer.c_str(), pathLen);
|
|
offset += pathLen;
|
|
errorBuffer[offset++] = 0x80 | (errorCode & 0x7F); // Error flag + error code
|
|
errorBuffer[offset++] = 0; // totalFiles low = 0
|
|
errorBuffer[offset++] = 0; // totalFiles high = 0
|
|
errorBuffer[offset++] = 0; // fileCount = 0
|
|
|
|
clients.notifyAllBinary(NotificationType::FileSystem, errorBuffer, offset);
|
|
}
|
|
|
|
// Send binary result for file action (delete, rename, etc.)
|
|
static void sendBinaryFileActionResult(uint8_t action, bool success, uint8_t errorCode, const char* path = nullptr) {
|
|
static uint8_t resultBuffer[260];
|
|
size_t offset = 0;
|
|
uint8_t pathLen = path ? (uint8_t)strlen(path) : 0;
|
|
|
|
resultBuffer[offset++] = MSG_FILE_ACTION_RESULT;
|
|
resultBuffer[offset++] = action;
|
|
resultBuffer[offset++] = success ? 0 : 1;
|
|
resultBuffer[offset++] = errorCode;
|
|
resultBuffer[offset++] = pathLen;
|
|
if (pathLen > 0) {
|
|
memcpy(resultBuffer + offset, path, pathLen);
|
|
offset += pathLen;
|
|
}
|
|
|
|
clients.notifyAllBinary(NotificationType::FileSystem, resultBuffer, offset);
|
|
}
|
|
|
|
// Load file data
|
|
static bool handleLoadFileData(const uint8_t* data, size_t len) {
|
|
if (len < 2) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathLength = data[0];
|
|
uint8_t pathType = data[1];
|
|
|
|
if (len < 2 + pathLength) {
|
|
return false;
|
|
}
|
|
|
|
// Use helper function to build path
|
|
const char* path = (pathLength > 0) ? reinterpret_cast<const char*>(data + 2) : nullptr;
|
|
buildFullPath(pathType, path, pathLength, pathBuffer);
|
|
|
|
ESP_LOGI("FileCommands", "Final path: '%s'", pathBuffer.c_str());
|
|
|
|
// Check file existence
|
|
fs::FS& fs = getFS(pathType);
|
|
if (!fs.exists(pathBuffer.c_str())) {
|
|
sendBinaryFileActionResult(7, false, 3, pathBuffer.c_str()); // 7=load, error 3=not found
|
|
return true;
|
|
}
|
|
|
|
// STREAM file directly to BLE (NO buffering entire file!)
|
|
File file = fs.open(pathBuffer.c_str(), FILE_READ);
|
|
if (!file) {
|
|
sendBinaryFileActionResult(7, false, 13, pathBuffer.c_str()); // error 13=failed to open
|
|
return true;
|
|
}
|
|
|
|
size_t fileSize = file.size();
|
|
ESP_LOGI("FileCommands", "Streaming file: %zu bytes", fileSize);
|
|
|
|
// Guard: keep file streaming small and predictable for BLE.
|
|
// App requirement: support loading up to 10 KB.
|
|
static const size_t MAX_STREAM_FILE_SIZE = 10 * 1024;
|
|
if (fileSize > MAX_STREAM_FILE_SIZE) {
|
|
file.close();
|
|
sendBinaryFileActionResult(7, false, 16, pathBuffer.c_str()); // 16=file too large
|
|
return true;
|
|
}
|
|
|
|
// Build header: [0xA0][pathLen:1][path][fileSize:4]
|
|
size_t fullPathLen = strlen(pathBuffer.c_str());
|
|
const size_t MAX_HEADER_SIZE = 256;
|
|
uint8_t header[MAX_HEADER_SIZE];
|
|
|
|
if (1 + 1 + fullPathLen + 4 > MAX_HEADER_SIZE) {
|
|
file.close();
|
|
sendBinaryFileActionResult(7, false, 14, "Path too long"); // error 14=path too long
|
|
return true;
|
|
}
|
|
|
|
size_t offset = 0;
|
|
header[offset++] = 0xA0; // MSG_FILE_CONTENT
|
|
header[offset++] = (uint8_t)fullPathLen;
|
|
memcpy(header + offset, pathBuffer.c_str(), fullPathLen);
|
|
offset += fullPathLen;
|
|
|
|
// File size (4 bytes, little-endian)
|
|
header[offset++] = (fileSize >> 0) & 0xFF;
|
|
header[offset++] = (fileSize >> 8) & 0xFF;
|
|
header[offset++] = (fileSize >> 16) & 0xFF;
|
|
header[offset++] = (fileSize >> 24) & 0xFF;
|
|
|
|
size_t headerSize = offset;
|
|
|
|
// TRUE STREAMING: Use BLE adapter's streaming method
|
|
BleAdapter* bleAdapter = BleAdapter::getInstance();
|
|
if (bleAdapter != nullptr) {
|
|
bleAdapter->streamFileData(header, headerSize, file, fileSize);
|
|
file.close();
|
|
} else {
|
|
file.close();
|
|
sendBinaryFileActionResult(7, false, 15, "BLE adapter not found"); // error 15=no adapter
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Recursively remove a directory and all its contents.
|
|
*
|
|
* Walks the directory tree depth-first: deletes every file, recurses
|
|
* into sub-directories, then removes the now-empty directory itself.
|
|
*
|
|
* @param fs Filesystem reference (SD or LittleFS).
|
|
* @param path Absolute path of the directory to remove.
|
|
* @return true if the directory and all children were deleted.
|
|
*/
|
|
static bool removeDirectoryRecursive(fs::FS& fs, const char* path) {
|
|
File dir = fs.open(path);
|
|
if (!dir || !dir.isDirectory()) {
|
|
dir.close();
|
|
return false;
|
|
}
|
|
|
|
bool allOk = true;
|
|
File child = dir.openNextFile();
|
|
while (child) {
|
|
// Copy path before close — child.path() is an internal pointer
|
|
// that becomes invalid after child.close().
|
|
char childPathBuf[256];
|
|
strncpy(childPathBuf, child.path(), sizeof(childPathBuf) - 1);
|
|
childPathBuf[sizeof(childPathBuf) - 1] = '\0';
|
|
bool isDir = child.isDirectory();
|
|
child.close();
|
|
|
|
if (isDir) {
|
|
if (!removeDirectoryRecursive(fs, childPathBuf)) {
|
|
ESP_LOGE("FileCmd", "Failed to remove dir: %s", childPathBuf);
|
|
allOk = false;
|
|
// Continue deleting other entries instead of aborting
|
|
}
|
|
} else {
|
|
if (!fs.remove(childPathBuf)) {
|
|
ESP_LOGE("FileCmd", "Failed to remove file: %s", childPathBuf);
|
|
allOk = false;
|
|
}
|
|
}
|
|
// Yield to prevent watchdog timeout on deep/large trees
|
|
vTaskDelay(1);
|
|
child = dir.openNextFile();
|
|
}
|
|
dir.close();
|
|
|
|
// Directory should now be empty — remove it
|
|
if (!fs.rmdir(path)) {
|
|
ESP_LOGE("FileCmd", "Failed to rmdir: %s", path);
|
|
return false;
|
|
}
|
|
return allOk;
|
|
}
|
|
|
|
static bool handleRemoveFile(const uint8_t* data, size_t len) {
|
|
if (len < 2) {
|
|
sendBinaryFileActionResult(1, false, 1); // 1=delete, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathLength = data[0];
|
|
uint8_t pathType = data[1];
|
|
if (len < 2 + pathLength) {
|
|
sendBinaryFileActionResult(1, false, 2); // error 2=path length mismatch
|
|
return false;
|
|
}
|
|
|
|
// Build full path using helper function
|
|
const char* path = reinterpret_cast<const char*>(data + 2);
|
|
buildFullPath(pathType, path, pathLength, pathBuffer);
|
|
|
|
// Check if path exists
|
|
fs::FS& fs = getFS(pathType);
|
|
if (!fs.exists(pathBuffer.c_str())) {
|
|
sendBinaryFileActionResult(1, false, 3, pathBuffer.c_str()); // error 3=not found
|
|
return false;
|
|
}
|
|
|
|
// Check if it's a directory or file and remove accordingly
|
|
File file = fs.open(pathBuffer.c_str());
|
|
bool isDirectory = file.isDirectory();
|
|
file.close();
|
|
|
|
bool ok = false;
|
|
if (isDirectory) {
|
|
ok = removeDirectoryRecursive(fs, pathBuffer.c_str());
|
|
} else {
|
|
ok = fs.remove(pathBuffer.c_str());
|
|
}
|
|
|
|
sendBinaryFileActionResult(1, ok, ok ? 0 : 4, pathBuffer.c_str()); // error 4=delete failed
|
|
return ok;
|
|
}
|
|
|
|
/**
|
|
* @brief Format SD card: recursively delete all contents and re-create
|
|
* the default directory structure.
|
|
* Sends progressive feedback (errorCode 0xFF = in-progress step).
|
|
*
|
|
* Payload: [0x46][0x53] ('FS') as confirmation guard — prevents
|
|
* accidental invocation.
|
|
*/
|
|
static bool handleFormatSDCard(const uint8_t* data, size_t len) {
|
|
// Require 2-byte confirmation payload 'FS' (Format SD)
|
|
if (len < 2 || data[0] != 0x46 || data[1] != 0x53) {
|
|
ESP_LOGW("FileCmd", "Format SD rejected: missing confirmation 'FS'");
|
|
sendBinaryFileActionResult(8, false, 1); // actionType 8 = format
|
|
return false;
|
|
}
|
|
|
|
ESP_LOGW("FileCmd", "FORMAT SD CARD — deleting all contents");
|
|
|
|
// Phase 1: notify app that format has started
|
|
sendBinaryFileActionResult(8, true, 0xFF, "Starting format...");
|
|
vTaskDelay(pdMS_TO_TICKS(50)); // Let BLE send the notification
|
|
|
|
// Phase 2: recursively delete every entry in SD root
|
|
File root = SD.open("/");
|
|
if (!root || !root.isDirectory()) {
|
|
ESP_LOGE("FileCmd", "Cannot open SD root");
|
|
sendBinaryFileActionResult(8, false, 2);
|
|
return false;
|
|
}
|
|
|
|
bool allOk = true;
|
|
int deletedCount = 0;
|
|
File child = root.openNextFile();
|
|
while (child) {
|
|
// Copy path to local buffer before close — child.path()
|
|
// returns an internal pointer invalidated by close().
|
|
char childPathBuf[256];
|
|
strncpy(childPathBuf, child.path(), sizeof(childPathBuf) - 1);
|
|
childPathBuf[sizeof(childPathBuf) - 1] = '\0';
|
|
bool isDir = child.isDirectory();
|
|
child.close();
|
|
|
|
// Send progress notification for each item being deleted
|
|
char progressMsg[280];
|
|
snprintf(progressMsg, sizeof(progressMsg), "Deleting: %s", childPathBuf);
|
|
sendBinaryFileActionResult(8, true, 0xFF, progressMsg);
|
|
vTaskDelay(pdMS_TO_TICKS(20)); // Let BLE send + prevent WDT
|
|
|
|
if (isDir) {
|
|
if (!removeDirectoryRecursive(SD, childPathBuf)) {
|
|
ESP_LOGE("FileCmd", "Failed to remove dir: %s", childPathBuf);
|
|
allOk = false;
|
|
}
|
|
} else {
|
|
if (!SD.remove(childPathBuf)) {
|
|
ESP_LOGE("FileCmd", "Failed to remove file: %s", childPathBuf);
|
|
allOk = false;
|
|
}
|
|
}
|
|
deletedCount++;
|
|
// Yield to prevent watchdog timeout during format
|
|
vTaskDelay(1);
|
|
child = root.openNextFile();
|
|
}
|
|
root.close();
|
|
|
|
ESP_LOGI("FileCmd", "Deleted %d items from SD root", deletedCount);
|
|
|
|
// Phase 3: re-create default directory structure with progress and verification
|
|
static const char* defaultDirs[] = {
|
|
"/DATA",
|
|
"/DATA/RECORDS",
|
|
"/DATA/SIGNALS",
|
|
"/DATA/PRESETS",
|
|
"/DATA/TEMP",
|
|
"/DATA/PROTOPIRATE"
|
|
};
|
|
bool creationSuccess = true;
|
|
for (int i = 0; i < 6; i++) {
|
|
char progressMsg[280];
|
|
snprintf(progressMsg, sizeof(progressMsg), "Creating: %s", defaultDirs[i]);
|
|
sendBinaryFileActionResult(8, true, 0xFF, progressMsg);
|
|
vTaskDelay(pdMS_TO_TICKS(20));
|
|
|
|
// Create directory and verify
|
|
if (!SD.mkdir(defaultDirs[i])) {
|
|
// mkdir returns false if directory already exists or creation failed
|
|
// Check if it exists to distinguish between these cases
|
|
if (!SD.exists(defaultDirs[i])) {
|
|
ESP_LOGE("FileCmd", "Failed to create directory: %s", defaultDirs[i]);
|
|
creationSuccess = false;
|
|
allOk = false;
|
|
} else {
|
|
ESP_LOGI("FileCmd", "Directory already exists: %s", defaultDirs[i]);
|
|
}
|
|
} else {
|
|
ESP_LOGI("FileCmd", "Created directory: %s", defaultDirs[i]);
|
|
}
|
|
}
|
|
|
|
ESP_LOGI("FileCmd", "SD card format %s", allOk ? "complete" : "completed with errors");
|
|
// Send final result (errorCode 0 = done successfully, 4 = done with errors)
|
|
sendBinaryFileActionResult(8, allOk, allOk ? 0 : 4);
|
|
return allOk;
|
|
}
|
|
|
|
static bool handleRenameFile(const uint8_t* data, size_t len) {
|
|
if (len < 3) {
|
|
sendBinaryFileActionResult(2, false, 1); // 2=rename, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathType = data[0];
|
|
uint8_t fromLength = data[1];
|
|
if (len < 2 + fromLength + 1) {
|
|
sendBinaryFileActionResult(2, false, 5); // error 5=to length missing
|
|
return false;
|
|
}
|
|
|
|
const char* fromPtr = reinterpret_cast<const char*>(data + 2);
|
|
uint8_t toLength = data[2 + fromLength];
|
|
if (len < 3 + fromLength + toLength) {
|
|
sendBinaryFileActionResult(2, false, 2); // error 2=path length mismatch
|
|
return false;
|
|
}
|
|
const char* toPtr = reinterpret_cast<const char*>(data + 3 + fromLength);
|
|
|
|
// Build full paths using helper functions
|
|
buildFullPath(pathType, fromPtr, fromLength, pathBuffer);
|
|
|
|
// Use a temporary PathBuffer for "to" path (we need both paths)
|
|
static PathBuffer toPathBuffer;
|
|
buildFullPath(pathType, toPtr, toLength, toPathBuffer);
|
|
|
|
fs::FS& fs = getFS(pathType);
|
|
bool ok = fs.exists(pathBuffer.c_str()) && fs.rename(pathBuffer.c_str(), toPathBuffer.c_str());
|
|
|
|
sendBinaryFileActionResult(2, ok, ok ? 0 : 6, toPathBuffer.c_str()); // 2=rename, error 6=rename failed
|
|
return ok;
|
|
}
|
|
|
|
static bool handleCreateDirectory(const uint8_t* data, size_t len) {
|
|
if (len < 2) {
|
|
sendBinaryFileActionResult(3, false, 1); // 3=mkdir, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathLength = data[0];
|
|
uint8_t pathType = data[1];
|
|
if (len < 2 + pathLength) {
|
|
sendBinaryFileActionResult(3, false, 2); // error 2=path length mismatch
|
|
return false;
|
|
}
|
|
|
|
// Build paths using helper functions
|
|
const char* dirPtr = reinterpret_cast<const char*>(data + 2);
|
|
buildFullPath(pathType, dirPtr, pathLength, pathBuffer);
|
|
|
|
// Get base directory for checking/creating
|
|
static PathBuffer baseDirBuffer;
|
|
buildBasePath(pathType, baseDirBuffer);
|
|
|
|
fs::FS& fs = getFS(pathType);
|
|
// Create base directory if needed
|
|
if (baseDirBuffer.size() > 0) {
|
|
if (!fs.exists(baseDirBuffer.c_str())) {
|
|
fs.mkdir(baseDirBuffer.c_str());
|
|
}
|
|
}
|
|
|
|
// Recursive mkdir — create each path segment from root to leaf
|
|
// SD/LittleFS mkdir() is NOT recursive, so we walk each '/' level
|
|
const char* fullStr = pathBuffer.c_str();
|
|
size_t fullLen = strlen(fullStr);
|
|
bool ok = true;
|
|
{
|
|
char segment[256];
|
|
strncpy(segment, fullStr, sizeof(segment) - 1);
|
|
segment[sizeof(segment) - 1] = '\0';
|
|
for (size_t i = 1; i < fullLen; i++) {
|
|
if (segment[i] == '/') {
|
|
segment[i] = '\0';
|
|
if (!fs.exists(segment)) {
|
|
if (!fs.mkdir(segment)) {
|
|
ESP_LOGW("FileCommands", "mkdir failed for segment: %s", segment);
|
|
}
|
|
}
|
|
segment[i] = '/';
|
|
}
|
|
}
|
|
}
|
|
// Create the final directory itself
|
|
if (!fs.exists(fullStr)) {
|
|
ok = fs.mkdir(fullStr);
|
|
}
|
|
|
|
sendBinaryFileActionResult(3, ok, ok ? 0 : 7, pathBuffer.c_str()); // 3=mkdir, error 7=mkdir failed
|
|
return ok;
|
|
}
|
|
|
|
static bool handleSaveToSignalsWithName(const uint8_t* data, size_t len) {
|
|
if (len < 3) {
|
|
sendBinaryFileActionResult(4, false, 1); // 4=copy, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
// Parse source path length, target name length, and path type
|
|
uint8_t sourcePathLength = data[0];
|
|
uint8_t targetNameLength = data[1];
|
|
uint8_t pathType = data[2];
|
|
|
|
if (len < 3 + sourcePathLength + targetNameLength) {
|
|
sendBinaryFileActionResult(4, false, 8); // error 8=path lengths mismatch
|
|
return false;
|
|
}
|
|
|
|
// Extract source path
|
|
if (sourcePathLength == 0 || sourcePathLength >= pathBuffer.capacity()) {
|
|
sendBinaryFileActionResult(4, false, 9); // error 9=invalid source length
|
|
return false;
|
|
}
|
|
|
|
const char* sourcePath = reinterpret_cast<const char*>(&data[3]);
|
|
pathBuffer.clear();
|
|
pathBuffer.append(sourcePath, sourcePathLength);
|
|
|
|
// Extract target name to temporary buffer
|
|
const char* targetName = reinterpret_cast<const char*>(&data[3 + sourcePathLength]);
|
|
static PathBuffer targetNameBuffer;
|
|
targetNameBuffer.clear();
|
|
targetNameBuffer.append(targetName, targetNameLength);
|
|
|
|
// Check if date is provided (4 bytes after target name)
|
|
uint32_t fileDate = 0;
|
|
bool hasDate = false;
|
|
size_t expectedLen = 3 + sourcePathLength + targetNameLength;
|
|
if (len >= expectedLen + 4) {
|
|
// Read date (Unix timestamp in seconds, little-endian)
|
|
fileDate = data[expectedLen] |
|
|
(data[expectedLen + 1] << 8) |
|
|
(data[expectedLen + 2] << 16) |
|
|
(data[expectedLen + 3] << 24);
|
|
hasDate = true;
|
|
ESP_LOGI("FileCommands", "Date bytes: %02X %02X %02X %02X -> timestamp=%lu",
|
|
data[expectedLen], data[expectedLen + 1], data[expectedLen + 2], data[expectedLen + 3],
|
|
(unsigned long)fileDate);
|
|
} else {
|
|
ESP_LOGI("FileCommands", "No date provided: len=%zu, expected=%zu", len, expectedLen + 4);
|
|
}
|
|
|
|
ESP_LOGI("FileCommands", "SaveToSignalsWithName: sourcePath=%s, targetName=%s, pathType=%d%s",
|
|
pathBuffer.c_str(), targetNameBuffer.c_str(), pathType,
|
|
hasDate ? ", date provided" : "");
|
|
|
|
// Build destination path using helper function
|
|
static PathBuffer destPathBuffer;
|
|
buildBasePath(pathType, destPathBuffer);
|
|
destPathBuffer.append("/");
|
|
destPathBuffer.append(targetNameBuffer.c_str());
|
|
|
|
// Source is always on SD (absolute path from RECORDS/SIGNALS)
|
|
// Destination uses the pathType filesystem
|
|
fs::FS& destFS = getFS(pathType);
|
|
|
|
// Check if source file exists (source is always SD)
|
|
if (!SD.exists(pathBuffer.c_str())) {
|
|
sendBinaryFileActionResult(4, false, 3, pathBuffer.c_str()); // error 3=not found
|
|
return false;
|
|
}
|
|
|
|
// Create destination directory if it doesn't exist
|
|
static PathBuffer baseDirBuffer;
|
|
buildBasePath(pathType, baseDirBuffer);
|
|
if (baseDirBuffer.size() > 0 && !destFS.exists(baseDirBuffer.c_str())) {
|
|
destFS.mkdir(baseDirBuffer.c_str());
|
|
}
|
|
|
|
// Copy file from source to destination
|
|
File sourceFile = SD.open(pathBuffer.c_str(), FILE_READ);
|
|
if (!sourceFile) {
|
|
sendBinaryFileActionResult(4, false, 10, pathBuffer.c_str()); // error 10=failed to open source
|
|
return false;
|
|
}
|
|
|
|
File destFile = destFS.open(destPathBuffer.c_str(), FILE_WRITE);
|
|
if (!destFile) {
|
|
sourceFile.close();
|
|
sendBinaryFileActionResult(4, false, 11, destPathBuffer.c_str()); // error 11=failed to create dest
|
|
return false;
|
|
}
|
|
|
|
// Buffer-based file copy (512 bytes at a time)
|
|
if (!bufferedFileCopy(sourceFile, destFile)) {
|
|
ESP_LOGE("FileCommands", "Buffered copy failed");
|
|
}
|
|
|
|
sourceFile.close();
|
|
destFile.close();
|
|
|
|
// Set file date if provided (must be done immediately after close, before releasing mutex)
|
|
if (hasDate && fileDate > 0) {
|
|
ESP_LOGI("FileCommands", "Setting file date: timestamp=%lu", (unsigned long)fileDate);
|
|
|
|
// Manual conversion from Unix timestamp to FAT date/time (avoid gmtime stack issues)
|
|
uint32_t days = fileDate / 86400;
|
|
uint32_t seconds = fileDate % 86400;
|
|
|
|
// Calculate year (simplified, good for 1980-2100)
|
|
uint32_t year = 1970;
|
|
uint32_t dayOfYear = days;
|
|
while (dayOfYear >= 365) {
|
|
bool isLeap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
|
uint32_t daysInYear = isLeap ? 366 : 365;
|
|
if (dayOfYear >= daysInYear) {
|
|
dayOfYear -= daysInYear;
|
|
year++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate month and day
|
|
uint32_t month = 1;
|
|
uint32_t day = dayOfYear + 1;
|
|
const uint8_t daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
|
bool isLeap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
|
|
|
for (uint32_t m = 0; m < 12; m++) {
|
|
uint32_t daysInM = daysInMonth[m];
|
|
if (m == 1 && isLeap) daysInM = 29;
|
|
if (day > daysInM) {
|
|
day -= daysInM;
|
|
month++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate hour, minute, second
|
|
uint32_t hour = seconds / 3600;
|
|
uint32_t minute = (seconds % 3600) / 60;
|
|
uint32_t second = seconds % 60;
|
|
|
|
ESP_LOGI("FileCommands", "Converted date: %04lu-%02lu-%02lu %02lu:%02lu:%02lu",
|
|
(unsigned long)year, (unsigned long)month, (unsigned long)day,
|
|
(unsigned long)hour, (unsigned long)minute, (unsigned long)second);
|
|
|
|
if (year >= 1980 && year < 2108) {
|
|
FILINFO fno;
|
|
fno.fname[0] = '\0';
|
|
|
|
// FAT date: bits 15-9=year-1980, 8-5=month, 4-0=day
|
|
fno.fdate = ((year - 1980) << 9) | (month << 5) | day;
|
|
// FAT time: bits 15-11=hour, 10-5=minute, 4-0=second/2
|
|
fno.ftime = (hour << 11) | (minute << 5) | (second / 2);
|
|
|
|
ESP_LOGI("FileCommands", "FAT date=0x%04X, time=0x%04X", fno.fdate, fno.ftime);
|
|
|
|
// Use FATFS directly with the destination path (no /sd prefix needed for f_utime)
|
|
// f_utime works with the path as used by SD library
|
|
ESP_LOGI("FileCommands", "Setting time on file: %s", destPathBuffer.c_str());
|
|
|
|
// Try to set time using f_utime directly (doesn't require file to be open)
|
|
FRESULT res = f_utime(destPathBuffer.c_str(), &fno);
|
|
if (res == FR_OK) {
|
|
ESP_LOGI("FileCommands", "File time set successfully");
|
|
} else {
|
|
ESP_LOGW("FileCommands", "f_utime failed: %d, trying with file open", res);
|
|
|
|
// Fallback: open file and try again
|
|
FIL file;
|
|
res = f_open(&file, destPathBuffer.c_str(), FA_WRITE | FA_OPEN_EXISTING);
|
|
if (res == FR_OK) {
|
|
// Try f_utime again with file open
|
|
res = f_utime(destPathBuffer.c_str(), &fno);
|
|
f_close(&file);
|
|
|
|
if (res == FR_OK) {
|
|
ESP_LOGI("FileCommands", "File time set successfully (with file open)");
|
|
} else {
|
|
ESP_LOGW("FileCommands", "f_utime failed even with file open: %d", res);
|
|
}
|
|
} else {
|
|
ESP_LOGW("FileCommands", "Failed to open file for time setting: %d", res);
|
|
}
|
|
}
|
|
} else {
|
|
ESP_LOGW("FileCommands", "Year %lu out of range (1980-2107)", (unsigned long)year);
|
|
}
|
|
}
|
|
|
|
ESP_LOGI("FileCommands", "File copied successfully: %s -> %s%s",
|
|
pathBuffer.c_str(), destPathBuffer.c_str(),
|
|
hasDate ? " (date preserved)" : "");
|
|
|
|
// Send success response
|
|
sendBinaryFileActionResult(4, true, 0, destPathBuffer.c_str());
|
|
|
|
return true;
|
|
}
|
|
|
|
// Copy file
|
|
static bool handleCopyFile(const uint8_t* data, size_t len) {
|
|
if (len < 3) {
|
|
sendBinaryFileActionResult(4, false, 1); // 4=copy, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathType = data[0];
|
|
uint8_t sourceLength = data[1];
|
|
if (len < 2 + sourceLength + 1) {
|
|
sendBinaryFileActionResult(4, false, 12); // error 12=dest length missing
|
|
return false;
|
|
}
|
|
|
|
const char* sourcePtr = reinterpret_cast<const char*>(data + 2);
|
|
uint8_t destLength = data[2 + sourceLength];
|
|
if (len < 3 + sourceLength + destLength) {
|
|
sendBinaryFileActionResult(4, false, 2); // error 2=path length mismatch
|
|
return false;
|
|
}
|
|
const char* destPtr = reinterpret_cast<const char*>(data + 3 + sourceLength);
|
|
|
|
// Build full paths
|
|
buildFullPath(pathType, sourcePtr, sourceLength, pathBuffer);
|
|
|
|
static PathBuffer destPathBuffer;
|
|
buildFullPath(pathType, destPtr, destLength, destPathBuffer);
|
|
|
|
// Use correct filesystem for the pathType
|
|
fs::FS& fs = getFS(pathType);
|
|
|
|
// Check if source exists
|
|
if (!fs.exists(pathBuffer.c_str())) {
|
|
sendBinaryFileActionResult(4, false, 3, pathBuffer.c_str()); // error 3=not found
|
|
return false;
|
|
}
|
|
|
|
// Copy file using buffered transfer
|
|
File sourceFile = fs.open(pathBuffer.c_str(), FILE_READ);
|
|
if (!sourceFile) {
|
|
sendBinaryFileActionResult(4, false, 10, pathBuffer.c_str());
|
|
return false;
|
|
}
|
|
|
|
File destFile = fs.open(destPathBuffer.c_str(), FILE_WRITE);
|
|
if (!destFile) {
|
|
sourceFile.close();
|
|
sendBinaryFileActionResult(4, false, 11, destPathBuffer.c_str());
|
|
return false;
|
|
}
|
|
|
|
if (!bufferedFileCopy(sourceFile, destFile)) {
|
|
ESP_LOGE("FileCommands", "Buffered copy failed");
|
|
}
|
|
|
|
sourceFile.close();
|
|
destFile.close();
|
|
|
|
sendBinaryFileActionResult(4, true, 0, destPathBuffer.c_str());
|
|
return true;
|
|
}
|
|
|
|
// Move file - supports different pathType for source and destination
|
|
// Format: [sourcePathType:1][destPathType:1][sourcePathLength:1][sourcePath:variable][destPathLength:1][destPath:variable]
|
|
static bool handleMoveFile(const uint8_t* data, size_t len) {
|
|
if (len < 4) {
|
|
sendBinaryFileActionResult(5, false, 1); // 5=move, error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t sourcePathType = data[0];
|
|
uint8_t destPathType = data[1];
|
|
uint8_t sourceLength = data[2];
|
|
if (len < 3 + sourceLength + 1) {
|
|
sendBinaryFileActionResult(5, false, 12); // error 12=dest length missing
|
|
return false;
|
|
}
|
|
|
|
const char* sourcePtr = reinterpret_cast<const char*>(data + 3);
|
|
uint8_t destLength = data[3 + sourceLength];
|
|
if (len < 4 + sourceLength + destLength) {
|
|
sendBinaryFileActionResult(5, false, 2); // error 2=path length mismatch
|
|
return false;
|
|
}
|
|
const char* destPtr = reinterpret_cast<const char*>(data + 4 + sourceLength);
|
|
|
|
// Build full paths using respective pathTypes
|
|
buildFullPath(sourcePathType, sourcePtr, sourceLength, pathBuffer);
|
|
|
|
static PathBuffer destPathBuffer;
|
|
buildFullPath(destPathType, destPtr, destLength, destPathBuffer);
|
|
|
|
// Use correct filesystem for each pathType
|
|
fs::FS& srcFS = getFS(sourcePathType);
|
|
fs::FS& dstFS = getFS(destPathType);
|
|
|
|
// Check if source exists
|
|
if (!srcFS.exists(pathBuffer.c_str())) {
|
|
sendBinaryFileActionResult(5, false, 3, pathBuffer.c_str()); // error 3=not found
|
|
return false;
|
|
}
|
|
|
|
// If moving within the same storage type, use rename (fast)
|
|
bool ok = false;
|
|
if (sourcePathType == destPathType) {
|
|
ok = srcFS.rename(pathBuffer.c_str(), destPathBuffer.c_str());
|
|
} else {
|
|
// Cross-storage move: copy then delete
|
|
File sourceFile = srcFS.open(pathBuffer.c_str(), FILE_READ);
|
|
if (!sourceFile) {
|
|
sendBinaryFileActionResult(5, false, 10, pathBuffer.c_str());
|
|
return false;
|
|
}
|
|
|
|
File destFile = dstFS.open(destPathBuffer.c_str(), FILE_WRITE);
|
|
if (!destFile) {
|
|
sourceFile.close();
|
|
sendBinaryFileActionResult(5, false, 11, destPathBuffer.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Buffer-based file copy
|
|
if (!bufferedFileCopy(sourceFile, destFile)) {
|
|
ESP_LOGE("FileCommands", "Cross-storage copy failed");
|
|
}
|
|
|
|
sourceFile.close();
|
|
destFile.close();
|
|
|
|
// Delete source file from source filesystem
|
|
ok = srcFS.remove(pathBuffer.c_str());
|
|
}
|
|
|
|
sendBinaryFileActionResult(5, ok, ok ? 0 : 6, destPathBuffer.c_str()); // 5=move, error 6=move failed
|
|
return ok;
|
|
}
|
|
|
|
// Collect directory paths (recursive, stores paths for streaming)
|
|
static void collectDirectoryPaths(const char* basePath, std::vector<String>& paths) {
|
|
char fatfsPath[256];
|
|
snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", basePath);
|
|
|
|
FF_DIR fatDir;
|
|
FILINFO fno;
|
|
FRESULT res = f_opendir(&fatDir, fatfsPath);
|
|
if (res != FR_OK) {
|
|
res = f_opendir(&fatDir, basePath);
|
|
if (res != FR_OK) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint16_t entriesProcessed = 0;
|
|
while (true) {
|
|
res = f_readdir(&fatDir, &fno);
|
|
if (res != FR_OK || fno.fname[0] == 0) {
|
|
break;
|
|
}
|
|
|
|
// Skip . and ..
|
|
if (fno.fname[0] == '.' && (fno.fname[1] == '\0' || (fno.fname[1] == '.' && fno.fname[2] == '\0'))) {
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a directory
|
|
bool isDir = (fno.fname[0] != 0 && (fno.fattrib & AM_DIR) != 0);
|
|
|
|
if (isDir) {
|
|
// Build full path
|
|
char dirPath[256];
|
|
if (strcmp(basePath, "/") == 0) {
|
|
snprintf(dirPath, sizeof(dirPath), "/%s", fno.fname);
|
|
} else {
|
|
snprintf(dirPath, sizeof(dirPath), "%s/%s", basePath, fno.fname);
|
|
}
|
|
|
|
// Add to paths list
|
|
paths.push_back(String(dirPath));
|
|
|
|
// Recurse into subdirectory
|
|
collectDirectoryPaths(dirPath, paths);
|
|
}
|
|
|
|
entriesProcessed++;
|
|
// Yield every 10 entries to prevent watchdog timeout
|
|
if (entriesProcessed % 10 == 0) {
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
}
|
|
}
|
|
|
|
f_closedir(&fatDir);
|
|
}
|
|
|
|
// Get directory tree (only directories, recursive) - STREAMING VERSION
|
|
// Format: [0xA2][pathType:1][flags:1][totalDirs:2][dirCount:2][paths...]
|
|
// flags: bit 0 (0x01) = hasMore, bit 7 (0x80) = error
|
|
// For each path: [pathLen:1][path:pathLen]
|
|
static bool handleGetDirectoryTree(const uint8_t* data, size_t len) {
|
|
if (len < 1) {
|
|
sendBinaryDirectoryTreeError(1); // error 1=insufficient data
|
|
return false;
|
|
}
|
|
|
|
uint8_t pathType = data[0];
|
|
buildBasePath(pathType, pathBuffer);
|
|
|
|
ESP_LOGI("FileCommands", "Getting directory tree for pathType=%d, basePath='%s'", pathType, pathBuffer.c_str());
|
|
|
|
// Check memory
|
|
if (ESP.getFreeHeap() < 3000) {
|
|
sendBinaryDirectoryTreeError(1); // error 1=insufficient memory
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// Collect all directory paths first
|
|
std::vector<String> paths;
|
|
collectDirectoryPaths(pathBuffer.c_str(), paths);
|
|
|
|
uint16_t totalDirs = paths.size();
|
|
ESP_LOGI("FileCommands", "Collected %d directories, starting stream", totalDirs);
|
|
|
|
// STREAMING: Use 2KB buffer, send multiple messages if needed
|
|
const size_t BUFFER_SIZE = 2048;
|
|
static uint8_t binaryBuffer[BUFFER_SIZE];
|
|
|
|
uint16_t dirsSent = 0;
|
|
size_t pathIndex = 0;
|
|
bool hasMorePaths = true;
|
|
|
|
while (hasMorePaths) {
|
|
size_t bufferOffset = 0;
|
|
|
|
// Build message header
|
|
binaryBuffer[bufferOffset++] = MSG_DIRECTORY_TREE; // 0xA2
|
|
binaryBuffer[bufferOffset++] = pathType;
|
|
|
|
size_t flagsOffset = bufferOffset++;
|
|
size_t totalDirsOffset = bufferOffset;
|
|
bufferOffset += 2; // totalDirs (2 bytes)
|
|
size_t dirCountOffset = bufferOffset;
|
|
bufferOffset += 2; // dirCount (2 bytes)
|
|
|
|
uint16_t dirsInThisMessage = 0;
|
|
|
|
// Add paths to buffer until full or all paths processed
|
|
while (pathIndex < paths.size() && bufferOffset < BUFFER_SIZE - 260) { // Leave 260 bytes margin for path
|
|
const String& path = paths[pathIndex];
|
|
size_t pathLen = path.length();
|
|
|
|
if (pathLen > 255) pathLen = 255; // Limit path length
|
|
|
|
// Check if path fits
|
|
if (bufferOffset + 1 + pathLen >= BUFFER_SIZE - 16) {
|
|
// Buffer full, send this message and continue with next
|
|
break;
|
|
}
|
|
|
|
binaryBuffer[bufferOffset++] = (uint8_t)pathLen;
|
|
memcpy(binaryBuffer + bufferOffset, path.c_str(), pathLen);
|
|
bufferOffset += pathLen;
|
|
|
|
dirsInThisMessage++;
|
|
dirsSent++;
|
|
pathIndex++;
|
|
}
|
|
|
|
// Check if more paths remaining
|
|
hasMorePaths = (pathIndex < paths.size());
|
|
|
|
// Update flags and counts
|
|
binaryBuffer[flagsOffset] = hasMorePaths ? 0x01 : 0x00;
|
|
|
|
// totalDirs: 0xFFFF if more coming, actual count if last message
|
|
if (hasMorePaths) {
|
|
binaryBuffer[totalDirsOffset] = 0xFF;
|
|
binaryBuffer[totalDirsOffset + 1] = 0xFF;
|
|
} else {
|
|
binaryBuffer[totalDirsOffset] = totalDirs & 0xFF;
|
|
binaryBuffer[totalDirsOffset + 1] = (totalDirs >> 8) & 0xFF;
|
|
}
|
|
|
|
// dirCount (little-endian)
|
|
binaryBuffer[dirCountOffset] = dirsInThisMessage & 0xFF;
|
|
binaryBuffer[dirCountOffset + 1] = (dirsInThisMessage >> 8) & 0xFF;
|
|
|
|
// Send this message
|
|
if (dirsInThisMessage > 0 || !hasMorePaths) {
|
|
clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset);
|
|
ESP_LOGI("FileCommands", "Directory tree chunk: %d dirs (total sent: %d/%d)",
|
|
dirsInThisMessage, dirsSent, totalDirs);
|
|
|
|
// Small delay to allow mobile app to process
|
|
if (hasMorePaths) {
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
}
|
|
}
|
|
|
|
ESP_LOGI("FileCommands", "Directory tree stream complete: %d directories sent", totalDirs);
|
|
return true;
|
|
|
|
} catch (...) {
|
|
sendBinaryDirectoryTreeError(5); // error 5=unknown error
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Send binary error response for directory tree
|
|
static void sendBinaryDirectoryTreeError(uint8_t errorCode) {
|
|
static uint8_t errorBuffer[16];
|
|
size_t offset = 0;
|
|
|
|
errorBuffer[offset++] = MSG_DIRECTORY_TREE;
|
|
errorBuffer[offset++] = 0; // pathType (unknown)
|
|
errorBuffer[offset++] = 0x80 | (errorCode & 0x7F); // Error flag + error code
|
|
errorBuffer[offset++] = 0; // totalDirs low = 0
|
|
errorBuffer[offset++] = 0; // totalDirs high = 0
|
|
errorBuffer[offset++] = 0; // dirCount low = 0
|
|
errorBuffer[offset++] = 0; // dirCount high = 0
|
|
|
|
clients.notifyAllBinary(NotificationType::FileSystem, errorBuffer, offset);
|
|
}
|
|
|
|
// File upload with chunking
|
|
// Note: Command 0x0D is no longer registered in CommandHandler
|
|
// Actual processing happens in BleAdapter::handleUploadChunk
|
|
// This method is kept for compatibility but should not be called
|
|
static bool handleUploadFile(const uint8_t* data, size_t len) {
|
|
// Command 0x0D is handled in BleAdapter::handleUploadChunk
|
|
// This method should not be called
|
|
return false;
|
|
}
|
|
};
|
|
// Static buffers
|
|
JsonBuffer FileCommands::jsonBuffer;
|
|
PathBuffer FileCommands::pathBuffer;
|
|
LogBuffer FileCommands::logBuffer;
|
|
|
|
#endif // FileCommands_h
|