From dfaa6bfb419c6d58ad354e2e37c6fe4d6de93b6f Mon Sep 17 00:00:00 2001 From: GHGiampy <83699429+GHGiampy@users.noreply.github.com> Date: Tue, 18 Jan 2022 07:56:11 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Long=20filename=20open/create/write?= =?UTF-8?q?=20(#23526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Scott Lahteine --- Marlin/Configuration_adv.h | 28 +- Marlin/src/gcode/eeprom/M500-M504.cpp | 2 + Marlin/src/gcode/host/M115.cpp | 8 +- Marlin/src/sd/SdBaseFile.cpp | 584 ++++++++++++++++++++++---- Marlin/src/sd/SdBaseFile.h | 22 +- Marlin/src/sd/cardreader.cpp | 6 +- buildroot/share/scripts/upload.py | 78 ++-- buildroot/tests/mega2560 | 2 +- 8 files changed, 582 insertions(+), 148 deletions(-) diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h index c1d9c1600357..80b1b613eebc 100644 --- a/Marlin/Configuration_adv.h +++ b/Marlin/Configuration_adv.h @@ -1525,33 +1525,23 @@ // LCD's font must contain the characters. Check your selected LCD language. //#define UTF_FILENAME_SUPPORT - // This allows hosts to request long names for files and folders with M33 - //#define LONG_FILENAME_HOST_SUPPORT + //#define LONG_FILENAME_HOST_SUPPORT // Get the long filename of a file/folder with 'M33 ' and list long filenames with 'M20 L' + //#define LONG_FILENAME_WRITE_SUPPORT // Create / delete files with long filenames via M28, M30, and Binary Transfer Protocol - // Enable this option to scroll long filenames in the SD card menu - //#define SCROLL_LONG_FILENAMES + //#define SCROLL_LONG_FILENAMES // Scroll long filenames in the SD card menu - // Leave the heaters on after Stop Print (not recommended!) - //#define SD_ABORT_NO_COOLDOWN + //#define SD_ABORT_NO_COOLDOWN // Leave the heaters on after Stop Print (not recommended!) /** - * This option allows you to abort SD printing when any endstop is triggered. - * This feature must be enabled with "M540 S1" or from the LCD menu. - * To have any effect, endstops must be enabled during SD printing. + * Abort SD printing when any endstop is triggered. + * This feature is enabled with 'M540 S1' or from the LCD menu. + * Endstops must be activated for this option to work. */ //#define SD_ABORT_ON_ENDSTOP_HIT - /** - * This option makes it easier to print the same SD Card file again. - * On print completion the LCD Menu will open with the file selected. - * You can just click to start the print, or navigate elsewhere. - */ - //#define SD_REPRINT_LAST_SELECTED_FILE + //#define SD_REPRINT_LAST_SELECTED_FILE // On print completion open the LCD Menu and select the same file - /** - * Auto-report SdCard status with M27 S - */ - //#define AUTO_REPORT_SD_STATUS + //#define AUTO_REPORT_SD_STATUS // Auto-report media status with 'M27 S' /** * Support for USB thumb drives using an Arduino USB Host Shield or diff --git a/Marlin/src/gcode/eeprom/M500-M504.cpp b/Marlin/src/gcode/eeprom/M500-M504.cpp index a1f295ebde06..412d0033558f 100644 --- a/Marlin/src/gcode/eeprom/M500-M504.cpp +++ b/Marlin/src/gcode/eeprom/M500-M504.cpp @@ -56,6 +56,8 @@ void GcodeSuite::M502() { /** * M503: print settings currently in memory * + * S : Include / exclude header comments in the output. (Default: S1) + * * With CONFIGURATION_EMBEDDING: * C : Save the full Marlin configuration to SD Card as "mc.zip" */ diff --git a/Marlin/src/gcode/host/M115.cpp b/Marlin/src/gcode/host/M115.cpp index 08943ed5f299..45e0061a5b84 100644 --- a/Marlin/src/gcode/host/M115.cpp +++ b/Marlin/src/gcode/host/M115.cpp @@ -154,6 +154,12 @@ void GcodeSuite::M115() { // LONG_FILENAME_HOST_SUPPORT (M33) cap_line(F("LONG_FILENAME"), ENABLED(LONG_FILENAME_HOST_SUPPORT)); + // LONG_FILENAME_WRITE_SUPPORT (M23, M28, M30...) + cap_line(F("LFN_WRITE"), ENABLED(LONG_FILENAME_WRITE_SUPPORT)); + + // CUSTOM_FIRMWARE_UPLOAD (M20 F) + cap_line(F("CUSTOM_FIRMWARE_UPLOAD"), ENABLED(CUSTOM_FIRMWARE_UPLOAD)); + // EXTENDED_M20 (M20 L) cap_line(F("EXTENDED_M20"), ENABLED(LONG_FILENAME_HOST_SUPPORT)); @@ -179,7 +185,7 @@ void GcodeSuite::M115() { cap_line(F("MEATPACK"), SERIAL_IMPL.has_feature(port, SerialFeature::MeatPack)); // CONFIG_EXPORT - cap_line(F("CONFIG_EXPORT"), ENABLED(CONFIG_EMBED_AND_SAVE_TO_SD)); + cap_line(F("CONFIG_EXPORT"), ENABLED(CONFIGURATION_EMBEDDING)); // Machine Geometry #if ENABLED(M115_GEOMETRY_REPORT) diff --git a/Marlin/src/sd/SdBaseFile.cpp b/Marlin/src/sd/SdBaseFile.cpp index b357495a3edb..64d0ad68bd80 100644 --- a/Marlin/src/sd/SdBaseFile.cpp +++ b/Marlin/src/sd/SdBaseFile.cpp @@ -89,6 +89,7 @@ bool SdBaseFile::addDirCluster() { } // cache a file's directory entry +// cache the current "dirBlock_" and return the entry at index "dirIndex_" // return pointer to cached entry or null for failure dir_t* SdBaseFile::cacheDirEntry(uint8_t action) { if (!vol_->cacheRawBlock(dirBlock_, action)) return nullptr; @@ -384,6 +385,20 @@ int8_t SdBaseFile::lsPrintNext(uint8_t flags, uint8_t indent) { return DIR_IS_FILE(&dir) ? 1 : 2; } +/** + * Calculate a checksum for an 8.3 filename + * + * \param name The 8.3 file name to calculate + * + * \return The checksum byte + */ +uint8_t lfn_checksum(const uint8_t *name) { + uint8_t sum = 0; + for (uint8_t i = 11; i; i--) + sum = ((sum & 1) << 7) + (sum >> 1) + *name++; + return sum; +} + // Format directory name field from a 8.3 name string bool SdBaseFile::make83Name(const char *str, uint8_t *name, const char **ptr) { uint8_t n = 7, // Max index until a dot is found @@ -430,6 +445,10 @@ bool SdBaseFile::mkdir(SdBaseFile *parent, const char *path, bool pFlag) { SdBaseFile *sub = &dir1; SdBaseFile *start = parent; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + uint8_t dlname[LONG_FILENAME_LENGTH]; + #endif + if (!parent || isOpen()) return false; if (*path == '/') { @@ -439,28 +458,31 @@ bool SdBaseFile::mkdir(SdBaseFile *parent, const char *path, bool pFlag) { parent = &dir2; } } - while (1) { - if (!make83Name(path, dname, &path)) return false; + + for (;;) { + if (!TERN(LONG_FILENAME_WRITE_SUPPORT, parsePath(path, dname, dlname, &path), make83Name(path, dname, &path))) return false; while (*path == '/') path++; if (!*path) break; - if (!sub->open(parent, dname, O_READ)) { - if (!pFlag || !sub->mkdir(parent, dname)) + if (!sub->open(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname), O_READ)) { + if (!pFlag || !sub->mkdir(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname))) return false; } if (parent != start) parent->close(); parent = sub; sub = parent != &dir1 ? &dir1 : &dir2; } - return mkdir(parent, dname); + return mkdir(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname)); } -bool SdBaseFile::mkdir(SdBaseFile *parent, const uint8_t dname[11]) { +bool SdBaseFile::mkdir(SdBaseFile *parent, const uint8_t dname[11] + OPTARG(LONG_FILENAME_WRITE_SUPPORT, const uint8_t dlname[LONG_FILENAME_LENGTH]) +) { if (ENABLED(SDCARD_READONLY)) return false; if (!parent->isDir()) return false; // create a normal file - if (!open(parent, dname, O_CREAT | O_EXCL | O_RDWR)) return false; + if (!open(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname), O_CREAT | O_EXCL | O_RDWR)) return false; // convert file to directory flags_ = O_READ; @@ -578,6 +600,10 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const char *path, uint8_t oflag) { SdBaseFile dir1, dir2; SdBaseFile *parent = dirFile, *sub = &dir1; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + uint8_t dlname[LONG_FILENAME_LENGTH]; + #endif + if (!dirFile || isOpen()) return false; if (*path == '/') { // Path starts with '/' @@ -589,90 +615,244 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const char *path, uint8_t oflag) { } for (;;) { - if (!make83Name(path, dname, &path)) return false; + if (!TERN(LONG_FILENAME_WRITE_SUPPORT, parsePath(path, dname, dlname, &path), make83Name(path, dname, &path))) return false; while (*path == '/') path++; if (!*path) break; - if (!sub->open(parent, dname, O_READ)) return false; + if (TERN0(LONG_FILENAME_WRITE_SUPPORT, !sub->open(parent, dname, dlname, O_READ))) return false; if (parent != dirFile) parent->close(); parent = sub; sub = parent != &dir1 ? &dir1 : &dir2; } - return open(parent, dname, oflag); + return open(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname), oflag); } -// open with filename in dname -bool SdBaseFile::open(SdBaseFile *dirFile, const uint8_t dname[11], uint8_t oflag) { +// open with filename in dname and long filename in dlname +bool SdBaseFile::open(SdBaseFile *dirFile, const uint8_t dname[11] + OPTARG(LONG_FILENAME_WRITE_SUPPORT, const uint8_t dlname[LONG_FILENAME_LENGTH]) + , uint8_t oflag +) { bool emptyFound = false, fileFound = false; - uint8_t index; + uint8_t index = 0; dir_t *p; - vol_ = dirFile->vol_; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + // LFN - Long File Name support + const bool useLFN = dlname[0] != 0; + bool lfnFileFound = false; + vfat_t *pvFat; + uint8_t emptyCount = 0, + emptyIndex = 0, + reqEntriesNum = useLFN ? getLFNEntriesNum((char*)dlname) + 1 : 1, + lfnNameLength = useLFN ? strlen((char*)dlname) : 0, + lfnName[LONG_FILENAME_LENGTH], + lfnSequenceNumber = 0, + lfnChecksum = 0; + #endif + // Rewind this dir + vol_ = dirFile->vol_; dirFile->rewind(); - // search for file + // search for file while (dirFile->curPosition_ < dirFile->fileSize_) { - index = 0xF & (dirFile->curPosition_ >> 5); - p = dirFile->readDirCache(); - if (!p) return false; + // Get absolute index position + index = (dirFile->curPosition_ >> 5) IF_DISABLED(LONG_FILENAME_WRITE_SUPPORT, & 0x0F); + // Get next entry + if (!(p = dirFile->readDirCache())) return false; + + // Check empty status: Is entry empty? if (p->name[0] == DIR_NAME_FREE || p->name[0] == DIR_NAME_DELETED) { - // remember first empty slot + // Count the contiguous available entries in which (eventually) fit the new dir entry, if it's a write operation if (!emptyFound) { - dirBlock_ = dirFile->vol_->cacheBlockNumber(); - dirIndex_ = index; - emptyFound = true; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + if (emptyCount == 0) emptyIndex = index; + // Incr empty entries counter + // If found the required empty entries, mark it + if (++emptyCount == reqEntriesNum) { + dirBlock_ = dirFile->vol_->cacheBlockNumber(); + dirIndex_ = index & 0xF; + emptyFound = true; + } + #else + dirBlock_ = dirFile->vol_->cacheBlockNumber(); + dirIndex_ = index; + emptyFound = true; + #endif } - // done if no entries follow + // Done if no entries follow if (p->name[0] == DIR_NAME_FREE) break; } - else if (!memcmp(dname, p->name, 11)) { - fileFound = true; - break; + else { // Entry not empty + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + // Reset empty counter + if (!emptyFound) emptyCount = 0; + // Search for SFN or LFN? + if (!useLFN) { + // Check using SFN: file found? + if (!memcmp(dname, p->name, 11)) { + fileFound = true; + break; + } + } + else { + // Check using LFN: LFN not found? continue search for LFN + if (!lfnFileFound) { + // Is this dir a LFN? + if (isDirLFN(p)) { + // Get VFat dir entry + pvFat = (vfat_t *) p; + // Get checksum from the last entry of the sequence + if (pvFat->sequenceNumber & 0x40) lfnChecksum = pvFat->checksum; + // Get LFN sequence number + lfnSequenceNumber = pvFat->sequenceNumber & 0x1F; + if WITHIN(lfnSequenceNumber, 1, reqEntriesNum) { + // Check checksum for all other entries with the starting checksum fetched before + if (lfnChecksum == pvFat->checksum) { + // Set chunk of LFN from VFAT entry into lfnName + getLFNName(pvFat, (char *)lfnName, lfnSequenceNumber); + // LFN found? + if (!strncasecmp((char*)dlname, (char*)lfnName, lfnNameLength)) lfnFileFound = true; + } + } + } + } + else { // Complete LFN found, check for related SFN + // Check if only the SFN checksum match because the filename may be different due to different truncation methods + if (!isDirLFN(p) && (lfnChecksum == lfn_checksum(p->name))) { + fileFound = true; + break; + } + else lfnFileFound = false; // SFN not valid for the LFN found, reset LFN FileFound + } + } + #else + + if (!memcmp(dname, p->name, 11)) { + fileFound = true; + break; + } + + #endif // LONG_FILENAME_WRITE_SUPPORT } } + if (fileFound) { // don't open existing file if O_EXCL if (oflag & O_EXCL) return false; + TERN_(LONG_FILENAME_WRITE_SUPPORT, index &= 0xF); } else { // don't create unless O_CREAT and O_WRITE if ((oflag & (O_CREAT | O_WRITE)) != (O_CREAT | O_WRITE)) return false; - if (emptyFound) { - index = dirIndex_; - p = cacheDirEntry(SdVolume::CACHE_FOR_WRITE); - if (!p) return false; - } - else { - if (dirFile->type_ == FAT_FILE_TYPE_ROOT_FIXED) return false; - // add and zero cluster for dirFile - first cluster is in cache for write - if (!dirFile->addDirCluster()) return false; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) - // use first entry in cluster - p = dirFile->vol_->cache()->dir; - index = 0; - } - // initialize as empty file - memset(p, 0, sizeof(*p)); - memcpy(p->name, dname, 11); + // Use bookmark index if found empty entries + if (emptyFound) index = emptyIndex; - // set timestamps - if (dateTime_) { - // call user date/time function - dateTime_(&p->creationDate, &p->creationTime); - } - else { - // use default date/time - p->creationDate = FAT_DEFAULT_DATE; - p->creationTime = FAT_DEFAULT_TIME; - } - p->lastAccessDate = p->creationDate; - p->lastWriteDate = p->creationDate; - p->lastWriteTime = p->creationTime; + // Make room for needed entries + while (emptyCount < reqEntriesNum) { + p = dirFile->readDirCache(); + if (!p) break; + emptyCount++; + } + while (emptyCount < reqEntriesNum) { + if (dirFile->type_ == FAT_FILE_TYPE_ROOT_FIXED) return false; + // add and zero cluster for dirFile - first cluster is in cache for write + if (!dirFile->addDirCluster()) return false; + emptyCount += dirFile->vol_->blocksPerCluster() * 16; + } + + // Move to 1st entry to write + if (!dirFile->seekSet(32 * index)) return false; + + // Dir entries write loop: [LFN] + SFN(1) + LOOP_L_N(dirWriteIdx, reqEntriesNum) { + index = (dirFile->curPosition_ / 32) & 0xF; + p = dirFile->readDirCache(); + // LFN or SFN Entry? + if (dirWriteIdx < reqEntriesNum - 1) { + // Write LFN Entries + pvFat = (vfat_t *) p; + // initialize as empty file + memset(pvFat, 0, sizeof(*pvFat)); + lfnSequenceNumber = (reqEntriesNum - dirWriteIdx - 1) & 0x1F; + pvFat->attributes = DIR_ATT_LONG_NAME; + pvFat->checksum = lfn_checksum(dname); + // Set sequence number and mark as last LFN entry if it's the 1st loop + pvFat->sequenceNumber = lfnSequenceNumber | (dirWriteIdx == 0 ? 0x40 : 0); + // Set LFN name block + setLFNName(pvFat, (char*)dlname, lfnSequenceNumber); + } + else { + // Write SFN Entry + // initialize as empty file + memset(p, 0, sizeof(*p)); + memcpy(p->name, dname, 11); + + // set timestamps + if (dateTime_) { + // call user date/time function + dateTime_(&p->creationDate, &p->creationTime); + } + else { + // use default date/time + p->creationDate = FAT_DEFAULT_DATE; + p->creationTime = FAT_DEFAULT_TIME; + } + p->lastAccessDate = p->creationDate; + p->lastWriteDate = p->creationDate; + p->lastWriteTime = p->creationTime; + } + + // write entry to SD + dirFile->vol_->cacheSetDirty(); + if (!dirFile->vol_->cacheFlush()) return false; + } + + #else // !LONG_FILENAME_WRITE_SUPPORT + + if (emptyFound) { + index = dirIndex_; + p = cacheDirEntry(SdVolume::CACHE_FOR_WRITE); + if (!p) return false; + } + else { + if (dirFile->type_ == FAT_FILE_TYPE_ROOT_FIXED) return false; + + // add and zero cluster for dirFile - first cluster is in cache for write + if (!dirFile->addDirCluster()) return false; + + // use first entry in cluster + p = dirFile->vol_->cache()->dir; + index = 0; + } + + // initialize as empty file + memset(p, 0, sizeof(*p)); + memcpy(p->name, dname, 11); + + // set timestamps + if (dateTime_) { + // call user date/time function + dateTime_(&p->creationDate, &p->creationTime); + } + else { + // use default date/time + p->creationDate = FAT_DEFAULT_DATE; + p->creationTime = FAT_DEFAULT_TIME; + } + + p->lastAccessDate = p->creationDate; + p->lastWriteDate = p->creationDate; + p->lastWriteTime = p->creationTime; + + // write entry to SD + if (!dirFile->vol_->cacheFlush()) return false; + + #endif // !LONG_FILENAME_WRITE_SUPPORT - // write entry to SD - if (!dirFile->vol_->cacheFlush()) return false; } // open entry in cache return openCachedEntry(index, oflag); @@ -808,6 +988,191 @@ bool SdBaseFile::openNext(SdBaseFile *dirFile, uint8_t oflag) { return false; } +#if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + + /** + * Check if dir is a long file name entry (LFN) + * + * \param[in] dir Parent of this directory will be opened. Must not be root. + * \return true if the dir is a long file name entry (LFN) + */ + bool SdBaseFile::isDirLFN(const dir_t* dir) { + if (DIR_IS_LONG_NAME(dir)) { + vfat_t *VFAT = (vfat_t*)dir; + // Sanity-check the VFAT entry. The first cluster is always set to zero. And the sequence number should be higher than 0 + if ((VFAT->firstClusterLow == 0) && WITHIN((VFAT->sequenceNumber & 0x1F), 1, MAX_VFAT_ENTRIES)) return true; + } + return false; + } + + /** + * Check if dirname string is a long file name (LFN) + * + * \param[in] dirname The string to check + * \return true if the dirname is a long file name (LFN) + * \return false if the dirname is a short file name 8.3 (SFN) + */ + bool SdBaseFile::isDirNameLFN(const char *dirname) { + uint8_t length = strlen(dirname); + uint8_t idx = length; + bool dotFound = false; + if (idx > 12) return true; // LFN due to filename length > 12 ("filename.ext") + // Check dot(s) position + while (idx) { + if (dirname[--idx] == '.') { + if (!dotFound) { + // Last dot (extension) is allowed only + // in position [1..8] from start or [0..3] from end for SFN else it's a LFN + // A filename starting with "." is a LFN (eg. ".file" ->in SFN-> "file~1 ") + // A filename ending with "." is a SFN (if length <= 9) (eg. "file." ->in SFN-> "file ") + if (idx > 8 || idx == 0 || (length - idx - 1) > 3) return true; // LFN due to dot extension position + dotFound = true; + } + else { + // Found another dot, is a LFN + return true; + } + } + } + // If no dots found, the filename must be of max 8 characters + if ((!dotFound) && length > 8) return true; // LFN due to max filename (without extension) length + return false; + } + + /** + * Parse path and return 8.3 format and LFN filenames (if the parsed path is a LFN) + * The SFN is without dot ("FILENAMEEXT") + * The LFN is complete ("Filename.ext") + */ + bool SdBaseFile::parsePath(const char *path, uint8_t *name, uint8_t *lname, const char **ptrNextPath) { + // Init randomizer for SFN generation + randomSeed(millis()); + // Parse the LFN + uint8_t ilfn = 0; + bool lastDotFound = false; + const char *pLastDot = 0; + const char *lfnpath = path; + uint8_t c; + + while (*lfnpath && *lfnpath != '/') { + if (ilfn == LONG_FILENAME_LENGTH - 1) return false; // Name too long + c = *lfnpath++; // Get char and advance + // Fail for illegal characters + PGM_P p = PSTR("|<>^+=?/[];:,*\"\\"); + while (uint8_t b = pgm_read_byte(p++)) if (b == c) return false; // Check reserved characters + if (c < 0x20 || c == 0x7F) return false; // Check non-printable characters + if (c == '.' && (lfnpath - 1) > path) { // Skip dot '.' check in 1st position + // Save last dot pointer (skip if starts with '.') + pLastDot = lfnpath - 1; + lastDotFound = true; + } + lname[ilfn++] = c; // Set LFN character + } + // Terminate LFN + lname[ilfn] = 0; + + // Parse/generate 8.3 SFN. Will take + // until 8 characters for the filename part + // until 3 characters for the extension part (if exists) + // Add 4 more characters if name part < 3 + // Add '~cnt' characters if it's a LFN + const bool isLFN = isDirNameLFN((char*)lname); + + uint8_t n = isLFN ? 5 : 7, // Max index for each component of the file: + // starting with 7 or 5 (if LFN) + // switch to 10 for extension if the last dot is found + i = 11; + while (i) name[--i] = ' '; // Set whole FILENAMEEXT to spaces + while (*path && *path != '/') { + c = *path++; // Get char and advance + // Skip spaces and dots (if it's not the last dot) + if (c == ' ') continue; + if (c == '.' && (!lastDotFound || (lastDotFound && path < pLastDot))) continue; + // Fail for illegal characters + PGM_P p = PSTR("|<>^+=?/[];:,*\"\\"); + while (uint8_t b = pgm_read_byte(p++)) if (b == c) return false; // Check reserved characters + if (c < 0x21 || c == 0x7F) return false; // Check non-printable characters + // Is last dot? + if (c == '.') { + // Switch to extension part + n = 10; + i = 8; + } + // If in valid range add the character + else if (i <= n) // Check size for 8.3 format + name[i++] = c + (WITHIN(c, 'a', 'z') ? 'A' - 'a' : 0); // Uppercase required for 8.3 name + } + // If it's a LFN then the SFN always need: + // - A minimal of 3 characters (otherwise 4 chars are added) + // - The '~cnt' at the end + if (isLFN) { + // Get the 1st free character + uint8_t iFree = 0; + while (1) if (name[iFree++] == ' ' || iFree == 11) break; + iFree--; + // Check minimal length + if (iFree < 3) { + // Append 4 extra characters + name[iFree++] = random(0,24) + 'A'; name[iFree++] = random(0,24) + 'A'; + name[iFree++] = random(0,24) + 'A'; name[iFree++] = random(0,24) + 'A'; + } + // Append '~cnt' characters + if (iFree > 5) iFree = 5; // Force the append in the last 3 characters of name part + name[iFree++] = '~'; + name[iFree++] = random(1,9) + '0'; + name[iFree++] = random(1,9) + '0'; + } + + // Check if LFN is needed + if (!isLFN) lname[0] = 0; // Zero LFN + *ptrNextPath = path; // Set passed pointer to the end + return name[0] != ' '; // Return true if any name was set + } + + /** + * Get the LFN filename block from a dir. Get the block in lname at startOffset + */ + void SdBaseFile::getLFNName(vfat_t *pFatDir, char *lname, uint8_t sequenceNumber) { + uint8_t startOffset = (sequenceNumber - 1) * FILENAME_LENGTH; + LOOP_L_N(i, FILENAME_LENGTH) { + const uint16_t utf16_ch = (i >= 11) ? pFatDir->name3[i - 11] : (i >= 5) ? pFatDir->name2[i - 5] : pFatDir->name1[i]; + #if ENABLED(UTF_FILENAME_SUPPORT) + // We can't reconvert to UTF-8 here as UTF-8 is variable-size encoding, but joining LFN blocks + // needs static bytes addressing. So here just store full UTF-16LE words to re-convert later. + uint16_t idx = (startOffset + i) * 2; // This is fixed as FAT LFN always contain UTF-16LE encoding + longFilename[idx] = utf16_ch & 0xFF; + longFilename[idx + 1] = (utf16_ch >> 8) & 0xFF; + #else + // Replace all multibyte characters to '_' + lname[startOffset + i] = (utf16_ch > 0xFF) ? '_' : (utf16_ch & 0xFF); + #endif + } + } + + /** + * Set the LFN filename block lname to a dir. Put the block based on sequence number + */ + void SdBaseFile::setLFNName(vfat_t *pFatDir, char *lname, uint8_t sequenceNumber) { + uint8_t startOffset = (sequenceNumber - 1) * FILENAME_LENGTH; + uint8_t nameLength = strlen(lname); + LOOP_L_N(i, FILENAME_LENGTH) { + uint16_t ch = 0; + if ((startOffset + i) < nameLength) + ch = lname[startOffset + i]; + else if ((startOffset + i) > nameLength) + ch = 0xFFFF; + // Set char + if (i < 5) + pFatDir->name1[i] = ch; + else if (i < 11) + pFatDir->name2[i - 5] = ch; + else + pFatDir->name3[i - 11] = ch; + } + } + +#endif // LONG_FILENAME_WRITE_SUPPORT + #if 0 /** * Open a directory's parent directory. @@ -1049,20 +1414,6 @@ int16_t SdBaseFile::read(void *buf, uint16_t nbyte) { return nbyte; } -/** - * Calculate a checksum for an 8.3 filename - * - * \param name The 8.3 file name to calculate - * - * \return The checksum byte - */ -uint8_t lfn_checksum(const uint8_t *name) { - uint8_t sum = 0; - for (uint8_t i = 11; i; i--) - sum = ((sum & 1) << 7) + (sum >> 1) + *name++; - return sum; -} - /** * Read the next entry in a directory. * @@ -1110,30 +1461,40 @@ int8_t SdBaseFile::readDir(dir_t *dir, char *longFilename) { if (VFAT->firstClusterLow == 0) { const uint8_t seq = VFAT->sequenceNumber & 0x1F; if (WITHIN(seq, 1, MAX_VFAT_ENTRIES)) { - n = (seq - 1) * (FILENAME_LENGTH); - if (n == 0) { + if (seq == 1) { checksum = VFAT->checksum; checksum_error = 0; } else if (checksum != VFAT->checksum) // orphan detected checksum_error = 1; - LOOP_L_N(i, FILENAME_LENGTH) { - const uint16_t utf16_ch = (i >= 11) ? VFAT->name3[i - 11] : (i >= 5) ? VFAT->name2[i - 5] : VFAT->name1[i]; - #if ENABLED(UTF_FILENAME_SUPPORT) - // We can't reconvert to UTF-8 here as UTF-8 is variable-size encoding, but joining LFN blocks - // needs static bytes addressing. So here just store full UTF-16LE words to re-convert later. - uint16_t idx = (n + i) * 2; // This is fixed as FAT LFN always contain UTF-16LE encoding - longFilename[idx] = utf16_ch & 0xFF; - longFilename[idx + 1] = (utf16_ch >> 8) & 0xFF; - #else - // Replace all multibyte characters to '_' - longFilename[n + i] = (utf16_ch > 0xFF) ? '_' : (utf16_ch & 0xFF); - #endif - } + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + + getLFNName(VFAT, longFilename, seq); // Get chunk of LFN from VFAT entry + + #else // !LONG_FILENAME_WRITE_SUPPORT + + n = (seq - 1) * (FILENAME_LENGTH); + + LOOP_L_N(i, FILENAME_LENGTH) { + const uint16_t utf16_ch = (i >= 11) ? VFAT->name3[i - 11] : (i >= 5) ? VFAT->name2[i - 5] : VFAT->name1[i]; + #if ENABLED(UTF_FILENAME_SUPPORT) + // We can't reconvert to UTF-8 here as UTF-8 is variable-size encoding, but joining LFN blocks + // needs static bytes addressing. So here just store full UTF-16LE words to re-convert later. + uint16_t idx = (n + i) * 2; // This is fixed as FAT LFN always contain UTF-16LE encoding + longFilename[idx] = utf16_ch & 0xFF; + longFilename[idx + 1] = (utf16_ch >> 8) & 0xFF; + #else + // Replace all multibyte characters to '_' + longFilename[n + i] = (utf16_ch > 0xFF) ? '_' : (utf16_ch & 0xFF); + #endif + } + + #endif // !LONG_FILENAME_WRITE_SUPPORT + // If this VFAT entry is the last one, add a NUL terminator at the end of the string if (VFAT->sequenceNumber & 0x40) - longFilename[(n + FILENAME_LENGTH) * LONG_FILENAME_CHARSIZE] = '\0'; + longFilename[LONG_FILENAME_CHARSIZE * TERN(LONG_FILENAME_WRITE_SUPPORT, seq * FILENAME_LENGTH, (n + FILENAME_LENGTH))] = '\0'; } } } @@ -1227,6 +1588,11 @@ bool SdBaseFile::remove() { dir_t *d = cacheDirEntry(SdVolume::CACHE_FOR_WRITE); if (!d) return false; + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + // get SFN checksum before name rewrite (needed for LFN deletion) + const uint8_t sfn_checksum = lfn_checksum(d->name); + #endif + // mark entry deleted d->name[0] = DIR_NAME_DELETED; @@ -1234,8 +1600,48 @@ bool SdBaseFile::remove() { type_ = FAT_FILE_TYPE_CLOSED; // write entry to SD - return vol_->cacheFlush(); - return true; + #if DISABLED(LONG_FILENAME_WRITE_SUPPORT) + + return vol_->cacheFlush(); + + #else // LONG_FILENAME_WRITE_SUPPORT + + flags_ = 0; + + if (!vol_->cacheFlush()) return false; + + // Check if the entry has a LFN + bool lastEntry = false; + // loop back to search for any LFN entries related to this file + LOOP_S_LE_N(sequenceNumber, 1, MAX_VFAT_ENTRIES) { + dirIndex_ = (dirIndex_ - 1) & 0xF; + if (dirBlock_ == 0) break; + if (dirIndex_ == 0xF) dirBlock_--; + dir_t *dir = cacheDirEntry(SdVolume::CACHE_FOR_WRITE); + if (!dir) return false; + + // check for valid LFN: not deleted, not top dirs (".", ".."), must be a LFN + if (dir->name[0] == DIR_NAME_DELETED || dir->name[0] == '.' || !isDirLFN(dir)) break; + // check coherent LFN: checksum and sequenceNumber must match + vfat_t* dirlfn = (vfat_t*) dir; + if (dirlfn->checksum != sfn_checksum || (dirlfn->sequenceNumber & 0x1F) != sequenceNumber) break; // orphan entry + // is last entry of LFN ? + lastEntry = (dirlfn->sequenceNumber & 0x40); + // mark as deleted + dirlfn->sequenceNumber = DIR_NAME_DELETED; + // Flush to SD + if (!vol_->cacheFlush()) return false; + // exit on last entry of LFN deleted + if (lastEntry) break; + } + + // Restore current index + //if (!seekSet(32UL * dirIndex_)) return false; + //dirIndex_ += prevDirIndex; + + return true; + + #endif // LONG_FILENAME_WRITE_SUPPORT } /** diff --git a/Marlin/src/sd/SdBaseFile.h b/Marlin/src/sd/SdBaseFile.h index 342edefb7079..bda44c6bd5c3 100644 --- a/Marlin/src/sd/SdBaseFile.h +++ b/Marlin/src/sd/SdBaseFile.h @@ -377,8 +377,26 @@ class SdBaseFile { dir_t* cacheDirEntry(uint8_t action); int8_t lsPrintNext(uint8_t flags, uint8_t indent); static bool make83Name(const char *str, uint8_t *name, const char **ptr); - bool mkdir(SdBaseFile *parent, const uint8_t dname[11]); - bool open(SdBaseFile *dirFile, const uint8_t dname[11], uint8_t oflag); + bool mkdir(SdBaseFile *parent, const uint8_t dname[11] + OPTARG(LONG_FILENAME_WRITE_SUPPORT, const uint8_t dlname[LONG_FILENAME_LENGTH]) + ); + bool open(SdBaseFile *dirFile, const uint8_t dname[11] + OPTARG(LONG_FILENAME_WRITE_SUPPORT, const uint8_t dlname[LONG_FILENAME_LENGTH]) + , uint8_t oflag + ); bool openCachedEntry(uint8_t cacheIndex, uint8_t oflags); dir_t* readDirCache(); + + // Long Filename create/write support + #if ENABLED(LONG_FILENAME_WRITE_SUPPORT) + static bool isDirLFN(const dir_t* dir); + static bool isDirNameLFN(const char *dirname); + static bool parsePath(const char *str, uint8_t *name, uint8_t *lname, const char **ptr); + /** + * Return the number of entries needed in the FAT for this LFN + */ + static inline uint8_t getLFNEntriesNum(const char *lname) { return (strlen(lname) + 12) / 13; } + static void getLFNName(vfat_t *vFatDir, char *lname, uint8_t startOffset); + static void setLFNName(vfat_t *vFatDir, char *lname, uint8_t lfnSequenceNumber); + #endif }; diff --git a/Marlin/src/sd/cardreader.cpp b/Marlin/src/sd/cardreader.cpp index 25f9d7d802f9..8c1d08a4605d 100644 --- a/Marlin/src/sd/cardreader.cpp +++ b/Marlin/src/sd/cardreader.cpp @@ -328,7 +328,7 @@ void CardReader::printListing( if (includeLongNames) { SERIAL_CHAR(' '); if (prependLong) { SERIAL_ECHO(prependLong); SERIAL_CHAR('/'); } - SERIAL_ECHO(longFilename[0] ? longFilename : "???"); + SERIAL_ECHO(longFilename[0] ? longFilename : filename); } #endif SERIAL_EOL(); @@ -385,9 +385,9 @@ void CardReader::ls( diveDir.rewind(); selectByName(diveDir, segment); - // Print /LongNamePart to serial output + // Print /LongNamePart to serial output or the short name if not available SERIAL_CHAR('/'); - SERIAL_ECHO(longFilename[0] ? longFilename : "???"); + SERIAL_ECHO(longFilename[0] ? longFilename : filename); // If the filename was printed then that's it if (!flag.filenameIsDir) break; diff --git a/buildroot/share/scripts/upload.py b/buildroot/share/scripts/upload.py index bfce4ea49dfd..ceb0db4f10ab 100644 --- a/buildroot/share/scripts/upload.py +++ b/buildroot/share/scripts/upload.py @@ -84,21 +84,26 @@ def _CheckSDCard(): #----------------# # File functions # #----------------# - def _GetFirmwareFiles(): + def _GetFirmwareFiles(UseLongFilenames): if Debug: print('Get firmware files...') - _Send('M20 F') + _Send(f"M20 F{'L' if UseLongFilenames else ''}") Responses = _Recv() if len(Responses) < 3 or not any('file list' in r for r in Responses): raise Exception('Error getting firmware files') if Debug: print('OK') return Responses - def _FilterFirmwareFiles(FirmwareList): + def _FilterFirmwareFiles(FirmwareList, UseLongFilenames): Firmwares = [] for FWFile in FirmwareList: - if not '/' in FWFile and '.BIN' in FWFile: - idx = FWFile.index('.BIN') - Firmwares.append(FWFile[:idx+4]) + # For long filenames take the 3rd column of the firmwares list + if UseLongFilenames: + Space = 0 + Space = FWFile.find(' ') + if Space >= 0: Space = FWFile.find(' ', Space + 1) + if Space >= 0: FWFile = FWFile[Space + 1:] + if not '/' in FWFile and '.BIN' in FWFile.upper(): + Firmwares.append(FWFile[:FWFile.upper().index('.BIN') + 4]) return Firmwares def _RemoveFirmwareFile(FirmwareFile): @@ -124,6 +129,8 @@ def _RemoveFirmwareFile(FirmwareFile): marlin_board_info_name = _GetMarlinEnv(MarlinEnv, 'BOARD_INFO_NAME') marlin_board_custom_build_flags = _GetMarlinEnv(MarlinEnv, 'BOARD_CUSTOM_BUILD_FLAGS') marlin_firmware_bin = _GetMarlinEnv(MarlinEnv, 'FIRMWARE_BIN') + marlin_long_filename_host_support = _GetMarlinEnv(MarlinEnv, 'LONG_FILENAME_HOST_SUPPORT') is not None + marlin_longname_write = _GetMarlinEnv(MarlinEnv, 'LONG_FILENAME_WRITE_SUPPORT') is not None marlin_custom_firmware_upload = _GetMarlinEnv(MarlinEnv, 'CUSTOM_FIRMWARE_UPLOAD') is not None marlin_short_build_version = _GetMarlinEnv(MarlinEnv, 'SHORT_BUILD_VERSION') marlin_string_config_h_author = _GetMarlinEnv(MarlinEnv, 'STRING_CONFIG_H_AUTHOR') @@ -148,6 +155,10 @@ def _RemoveFirmwareFile(FirmwareFile): # "upload_delete_old_bins": delete all *.bin files in the root of SD Card upload_delete_old_bins = marlin_motherboard in ['BOARD_CREALITY_V4', 'BOARD_CREALITY_V4210', 'BOARD_CREALITY_V423', 'BOARD_CREALITY_V427', 'BOARD_CREALITY_V431', 'BOARD_CREALITY_V452', 'BOARD_CREALITY_V453', 'BOARD_CREALITY_V24S1'] + # "upload_random_name": generate a random 8.3 firmware filename to upload + upload_random_filename = marlin_motherboard in ['BOARD_CREALITY_V4', 'BOARD_CREALITY_V4210', 'BOARD_CREALITY_V423', 'BOARD_CREALITY_V427', + 'BOARD_CREALITY_V431', 'BOARD_CREALITY_V452', 'BOARD_CREALITY_V453', 'BOARD_CREALITY_V24S1'] and not marlin_long_filename_host_support + try: # Start upload job @@ -156,28 +167,34 @@ def _RemoveFirmwareFile(FirmwareFile): # Dump some debug info if Debug: print('Upload using:') - print('---- Marlin --------------------') - print(f' PIOENV : {marlin_pioenv}') - print(f' SHORT_BUILD_VERSION : {marlin_short_build_version}') - print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}') - print(f' MOTHERBOARD : {marlin_motherboard}') - print(f' BOARD_INFO_NAME : {marlin_board_info_name}') - print(f' CUSTOM_BUILD_FLAGS : {marlin_board_custom_build_flags}') - print(f' FIRMWARE_BIN : {marlin_firmware_bin}') - print(f' CUSTOM_FIRMWARE_UPLOAD : {marlin_custom_firmware_upload}') - print('---- Upload parameters ---------') - print(f' Source : {upload_firmware_source_name}') - print(f' Target : {upload_firmware_target_name}') - print(f' Port : {upload_port} @ {upload_speed} baudrate') - print(f' Timeout : {upload_timeout}') - print(f' Block size : {upload_blocksize}') - print(f' Compression : {upload_compression}') - print(f' Error ratio : {upload_error_ratio}') - print(f' Test : {upload_test}') - print(f' Reset : {upload_reset}') - print('--------------------------------') + print('---- Marlin -----------------------------------') + print(f' PIOENV : {marlin_pioenv}') + print(f' SHORT_BUILD_VERSION : {marlin_short_build_version}') + print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}') + print(f' MOTHERBOARD : {marlin_motherboard}') + print(f' BOARD_INFO_NAME : {marlin_board_info_name}') + print(f' CUSTOM_BUILD_FLAGS : {marlin_board_custom_build_flags}') + print(f' FIRMWARE_BIN : {marlin_firmware_bin}') + print(f' LONG_FILENAME_HOST_SUPPORT : {marlin_long_filename_host_support}') + print(f' LONG_FILENAME_WRITE_SUPPORT : {marlin_longname_write}') + print(f' CUSTOM_FIRMWARE_UPLOAD : {marlin_custom_firmware_upload}') + print('---- Upload parameters ------------------------') + print(f' Source : {upload_firmware_source_name}') + print(f' Target : {upload_firmware_target_name}') + print(f' Port : {upload_port} @ {upload_speed} baudrate') + print(f' Timeout : {upload_timeout}') + print(f' Block size : {upload_blocksize}') + print(f' Compression : {upload_compression}') + print(f' Error ratio : {upload_error_ratio}') + print(f' Test : {upload_test}') + print(f' Reset : {upload_reset}') + print('-----------------------------------------------') # Custom implementations based on board parameters + # Generate a new 8.3 random filename + if upload_random_filename: + upload_firmware_target_name = f"fw-{''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=5))}.BIN" + print(f"Board {marlin_motherboard}: Overriding firmware filename to '{upload_firmware_target_name}'") # Delete all *.bin files on the root of SD Card (if flagged) if upload_delete_old_bins: @@ -185,11 +202,6 @@ def _RemoveFirmwareFile(FirmwareFile): if not marlin_custom_firmware_upload: raise Exception(f"CUSTOM_FIRMWARE_UPLOAD must be enabled in 'Configuration_adv.h' for '{marlin_motherboard}'") - # Generate a new 8.3 random filename - # This board remember the last firmware filename and doesn't allow to flash from that filename - upload_firmware_target_name = f"fw-{''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=5))}.BIN" - print(f"Board {marlin_motherboard}: Overriding firmware filename to '{upload_firmware_target_name}'") - # Init serial port port = serial.Serial(upload_port, baudrate = upload_speed, write_timeout = 0, timeout = 0.1) port.reset_input_buffer() @@ -198,13 +210,13 @@ def _RemoveFirmwareFile(FirmwareFile): _CheckSDCard() # Get firmware files - FirmwareFiles = _GetFirmwareFiles() + FirmwareFiles = _GetFirmwareFiles(marlin_long_filename_host_support) if Debug: for FirmwareFile in FirmwareFiles: print(f'Found: {FirmwareFile}') # Get all 1st level firmware files (to remove) - OldFirmwareFiles = _FilterFirmwareFiles(FirmwareFiles[1:len(FirmwareFiles)-2]) # Skip header and footers of list + OldFirmwareFiles = _FilterFirmwareFiles(FirmwareFiles[1:len(FirmwareFiles)-2], marlin_long_filename_host_support) # Skip header and footers of list if len(OldFirmwareFiles) == 0: print('No old firmware files to delete') else: diff --git a/buildroot/tests/mega2560 b/buildroot/tests/mega2560 index bf3290b9d02c..5ae9a2dbcf0b 100755 --- a/buildroot/tests/mega2560 +++ b/buildroot/tests/mega2560 @@ -35,7 +35,7 @@ opt_set MOTHERBOARD BOARD_AZTEEG_X3_PRO LCD_LANGUAGE jp_kana DEFAULT_EJERK 10 \ EXTRUDERS 5 TEMP_SENSOR_1 1 TEMP_SENSOR_2 5 TEMP_SENSOR_3 20 TEMP_SENSOR_4 1000 TEMP_SENSOR_BED 1 opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER LIGHTWEIGHT_UI SHOW_CUSTOM_BOOTSCREEN BOOT_MARLIN_LOGO_SMALL \ LCD_SET_PROGRESS_MANUALLY PRINT_PROGRESS_SHOW_DECIMALS SHOW_REMAINING_TIME STATUS_MESSAGE_SCROLLING SCROLL_LONG_FILENAMES \ - SDSUPPORT SDCARD_SORT_ALPHA NO_SD_AUTOSTART USB_FLASH_DRIVE_SUPPORT CANCEL_OBJECTS \ + SDSUPPORT LONG_FILENAME_WRITE_SUPPORT SDCARD_SORT_ALPHA NO_SD_AUTOSTART USB_FLASH_DRIVE_SUPPORT CANCEL_OBJECTS \ Z_PROBE_SLED AUTO_BED_LEVELING_UBL UBL_HILBERT_CURVE RESTORE_LEVELING_AFTER_G28 DEBUG_LEVELING_FEATURE G26_MESH_VALIDATION ENABLE_LEVELING_FADE_HEIGHT \ EEPROM_SETTINGS EEPROM_CHITCHAT GCODE_MACROS CUSTOM_MENU_MAIN \ MULTI_NOZZLE_DUPLICATION CLASSIC_JERK LIN_ADVANCE QUICK_HOME \