Long filename open/create/write (#23526)

Co-authored-by: Scott Lahteine <github@thinkyhead.com>
This commit is contained in:
GHGiampy 2022-01-18 07:56:11 +01:00 committed by Scott Lahteine
parent 8695f462b7
commit e704de9bb0
8 changed files with 582 additions and 148 deletions

View File

@ -1525,33 +1525,23 @@
// LCD's font must contain the characters. Check your selected LCD language. // LCD's font must contain the characters. Check your selected LCD language.
//#define UTF_FILENAME_SUPPORT //#define UTF_FILENAME_SUPPORT
// This allows hosts to request long names for files and folders with M33 //#define LONG_FILENAME_HOST_SUPPORT // Get the long filename of a file/folder with 'M33 <dosname>' and list long filenames with 'M20 L'
//#define LONG_FILENAME_HOST_SUPPORT //#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 // Scroll long filenames in the SD card menu
//#define SCROLL_LONG_FILENAMES
// Leave the heaters on after Stop Print (not recommended!) //#define SD_ABORT_NO_COOLDOWN // Leave the heaters on after Stop Print (not recommended!)
//#define SD_ABORT_NO_COOLDOWN
/** /**
* This option allows you to abort SD printing when any endstop is triggered. * Abort SD printing when any endstop is triggered.
* This feature must be enabled with "M540 S1" or from the LCD menu. * This feature is enabled with 'M540 S1' or from the LCD menu.
* To have any effect, endstops must be enabled during SD printing. * Endstops must be activated for this option to work.
*/ */
//#define SD_ABORT_ON_ENDSTOP_HIT //#define SD_ABORT_ON_ENDSTOP_HIT
/** //#define SD_REPRINT_LAST_SELECTED_FILE // On print completion open the LCD Menu and select the same file
* 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 AUTO_REPORT_SD_STATUS // Auto-report media status with 'M27 S<seconds>'
* Auto-report SdCard status with M27 S<seconds>
*/
//#define AUTO_REPORT_SD_STATUS
/** /**
* Support for USB thumb drives using an Arduino USB Host Shield or * Support for USB thumb drives using an Arduino USB Host Shield or

View File

@ -56,6 +56,8 @@ void GcodeSuite::M502() {
/** /**
* M503: print settings currently in memory * M503: print settings currently in memory
* *
* S<bool> : Include / exclude header comments in the output. (Default: S1)
*
* With CONFIGURATION_EMBEDDING: * With CONFIGURATION_EMBEDDING:
* C<flag> : Save the full Marlin configuration to SD Card as "mc.zip" * C<flag> : Save the full Marlin configuration to SD Card as "mc.zip"
*/ */

View File

@ -154,6 +154,12 @@ void GcodeSuite::M115() {
// LONG_FILENAME_HOST_SUPPORT (M33) // LONG_FILENAME_HOST_SUPPORT (M33)
cap_line(F("LONG_FILENAME"), ENABLED(LONG_FILENAME_HOST_SUPPORT)); 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) // EXTENDED_M20 (M20 L)
cap_line(F("EXTENDED_M20"), ENABLED(LONG_FILENAME_HOST_SUPPORT)); 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)); cap_line(F("MEATPACK"), SERIAL_IMPL.has_feature(port, SerialFeature::MeatPack));
// CONFIG_EXPORT // CONFIG_EXPORT
cap_line(F("CONFIG_EXPORT"), ENABLED(CONFIG_EMBED_AND_SAVE_TO_SD)); cap_line(F("CONFIG_EXPORT"), ENABLED(CONFIGURATION_EMBEDDING));
// Machine Geometry // Machine Geometry
#if ENABLED(M115_GEOMETRY_REPORT) #if ENABLED(M115_GEOMETRY_REPORT)

View File

@ -89,6 +89,7 @@ bool SdBaseFile::addDirCluster() {
} }
// cache a file's directory entry // 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 // return pointer to cached entry or null for failure
dir_t* SdBaseFile::cacheDirEntry(uint8_t action) { dir_t* SdBaseFile::cacheDirEntry(uint8_t action) {
if (!vol_->cacheRawBlock(dirBlock_, action)) return nullptr; 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; 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 // Format directory name field from a 8.3 name string
bool SdBaseFile::make83Name(const char *str, uint8_t *name, const char **ptr) { bool SdBaseFile::make83Name(const char *str, uint8_t *name, const char **ptr) {
uint8_t n = 7, // Max index until a dot is found 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 *sub = &dir1;
SdBaseFile *start = parent; SdBaseFile *start = parent;
#if ENABLED(LONG_FILENAME_WRITE_SUPPORT)
uint8_t dlname[LONG_FILENAME_LENGTH];
#endif
if (!parent || isOpen()) return false; if (!parent || isOpen()) return false;
if (*path == '/') { if (*path == '/') {
@ -439,28 +458,31 @@ bool SdBaseFile::mkdir(SdBaseFile *parent, const char *path, bool pFlag) {
parent = &dir2; 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++; while (*path == '/') path++;
if (!*path) break; if (!*path) break;
if (!sub->open(parent, dname, O_READ)) { if (!sub->open(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname), O_READ)) {
if (!pFlag || !sub->mkdir(parent, dname)) if (!pFlag || !sub->mkdir(parent, dname OPTARG(LONG_FILENAME_WRITE_SUPPORT, dlname)))
return false; return false;
} }
if (parent != start) parent->close(); if (parent != start) parent->close();
parent = sub; parent = sub;
sub = parent != &dir1 ? &dir1 : &dir2; 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 (ENABLED(SDCARD_READONLY)) return false;
if (!parent->isDir()) return false; if (!parent->isDir()) return false;
// create a normal file // 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 // convert file to directory
flags_ = O_READ; flags_ = O_READ;
@ -578,6 +600,10 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const char *path, uint8_t oflag) {
SdBaseFile dir1, dir2; SdBaseFile dir1, dir2;
SdBaseFile *parent = dirFile, *sub = &dir1; SdBaseFile *parent = dirFile, *sub = &dir1;
#if ENABLED(LONG_FILENAME_WRITE_SUPPORT)
uint8_t dlname[LONG_FILENAME_LENGTH];
#endif
if (!dirFile || isOpen()) return false; if (!dirFile || isOpen()) return false;
if (*path == '/') { // Path starts with '/' if (*path == '/') { // Path starts with '/'
@ -589,70 +615,178 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const char *path, uint8_t oflag) {
} }
for (;;) { 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++; while (*path == '/') path++;
if (!*path) break; 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(); if (parent != dirFile) parent->close();
parent = sub; parent = sub;
sub = parent != &dir1 ? &dir1 : &dir2; 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 // open with filename in dname and long filename in dlname
bool SdBaseFile::open(SdBaseFile *dirFile, const uint8_t dname[11], uint8_t oflag) { 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; bool emptyFound = false, fileFound = false;
uint8_t index; uint8_t index = 0;
dir_t *p; dir_t *p;
#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_; vol_ = dirFile->vol_;
dirFile->rewind(); dirFile->rewind();
// search for file // search for file
while (dirFile->curPosition_ < dirFile->fileSize_) { while (dirFile->curPosition_ < dirFile->fileSize_) {
index = 0xF & (dirFile->curPosition_ >> 5); // Get absolute index position
p = dirFile->readDirCache(); index = (dirFile->curPosition_ >> 5) IF_DISABLED(LONG_FILENAME_WRITE_SUPPORT, & 0x0F);
if (!p) return false;
// 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) { 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) { if (!emptyFound) {
#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(); dirBlock_ = dirFile->vol_->cacheBlockNumber();
dirIndex_ = index; dirIndex_ = index;
emptyFound = true; emptyFound = true;
#endif
} }
// done if no entries follow // Done if no entries follow
if (p->name[0] == DIR_NAME_FREE) break; if (p->name[0] == DIR_NAME_FREE) break;
} }
else if (!memcmp(dname, p->name, 11)) { 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; fileFound = true;
break; 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) { if (fileFound) {
// don't open existing file if O_EXCL // don't open existing file if O_EXCL
if (oflag & O_EXCL) return false; if (oflag & O_EXCL) return false;
TERN_(LONG_FILENAME_WRITE_SUPPORT, index &= 0xF);
} }
else { else {
// don't create unless O_CREAT and O_WRITE // don't create unless O_CREAT and O_WRITE
if ((oflag & (O_CREAT | O_WRITE)) != (O_CREAT | O_WRITE)) return false; 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;
#if ENABLED(LONG_FILENAME_WRITE_SUPPORT)
// Use bookmark index if found empty entries
if (emptyFound) index = emptyIndex;
// 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 // add and zero cluster for dirFile - first cluster is in cache for write
if (!dirFile->addDirCluster()) return false; if (!dirFile->addDirCluster()) return false;
emptyCount += dirFile->vol_->blocksPerCluster() * 16;
// use first entry in cluster
p = dirFile->vol_->cache()->dir;
index = 0;
} }
// 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 // initialize as empty file
memset(p, 0, sizeof(*p)); memset(p, 0, sizeof(*p));
memcpy(p->name, dname, 11); memcpy(p->name, dname, 11);
@ -670,9 +804,55 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const uint8_t dname[11], uint8_t ofla
p->lastAccessDate = p->creationDate; p->lastAccessDate = p->creationDate;
p->lastWriteDate = p->creationDate; p->lastWriteDate = p->creationDate;
p->lastWriteTime = p->creationTime; 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 // write entry to SD
if (!dirFile->vol_->cacheFlush()) return false; if (!dirFile->vol_->cacheFlush()) return false;
#endif // !LONG_FILENAME_WRITE_SUPPORT
} }
// open entry in cache // open entry in cache
return openCachedEntry(index, oflag); return openCachedEntry(index, oflag);
@ -808,6 +988,191 @@ bool SdBaseFile::openNext(SdBaseFile *dirFile, uint8_t oflag) {
return false; 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 #if 0
/** /**
* Open a directory's parent directory. * Open a directory's parent directory.
@ -1049,20 +1414,6 @@ int16_t SdBaseFile::read(void *buf, uint16_t nbyte) {
return 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. * Read the next entry in a directory.
* *
@ -1110,14 +1461,21 @@ int8_t SdBaseFile::readDir(dir_t *dir, char *longFilename) {
if (VFAT->firstClusterLow == 0) { if (VFAT->firstClusterLow == 0) {
const uint8_t seq = VFAT->sequenceNumber & 0x1F; const uint8_t seq = VFAT->sequenceNumber & 0x1F;
if (WITHIN(seq, 1, MAX_VFAT_ENTRIES)) { if (WITHIN(seq, 1, MAX_VFAT_ENTRIES)) {
n = (seq - 1) * (FILENAME_LENGTH); if (seq == 1) {
if (n == 0) {
checksum = VFAT->checksum; checksum = VFAT->checksum;
checksum_error = 0; checksum_error = 0;
} }
else if (checksum != VFAT->checksum) // orphan detected else if (checksum != VFAT->checksum) // orphan detected
checksum_error = 1; checksum_error = 1;
#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) { 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]; 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) #if ENABLED(UTF_FILENAME_SUPPORT)
@ -1131,9 +1489,12 @@ int8_t SdBaseFile::readDir(dir_t *dir, char *longFilename) {
longFilename[n + i] = (utf16_ch > 0xFF) ? '_' : (utf16_ch & 0xFF); longFilename[n + i] = (utf16_ch > 0xFF) ? '_' : (utf16_ch & 0xFF);
#endif #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 this VFAT entry is the last one, add a NUL terminator at the end of the string
if (VFAT->sequenceNumber & 0x40) 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); dir_t *d = cacheDirEntry(SdVolume::CACHE_FOR_WRITE);
if (!d) return false; 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 // mark entry deleted
d->name[0] = DIR_NAME_DELETED; d->name[0] = DIR_NAME_DELETED;
@ -1234,8 +1600,48 @@ bool SdBaseFile::remove() {
type_ = FAT_FILE_TYPE_CLOSED; type_ = FAT_FILE_TYPE_CLOSED;
// write entry to SD // write entry to SD
#if DISABLED(LONG_FILENAME_WRITE_SUPPORT)
return vol_->cacheFlush(); 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; return true;
#endif // LONG_FILENAME_WRITE_SUPPORT
} }
/** /**

View File

@ -377,8 +377,26 @@ class SdBaseFile {
dir_t* cacheDirEntry(uint8_t action); dir_t* cacheDirEntry(uint8_t action);
int8_t lsPrintNext(uint8_t flags, uint8_t indent); int8_t lsPrintNext(uint8_t flags, uint8_t indent);
static bool make83Name(const char *str, uint8_t *name, const char **ptr); static bool make83Name(const char *str, uint8_t *name, const char **ptr);
bool mkdir(SdBaseFile *parent, const uint8_t dname[11]); bool mkdir(SdBaseFile *parent, const uint8_t dname[11]
bool open(SdBaseFile *dirFile, const uint8_t dname[11], uint8_t oflag); 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); bool openCachedEntry(uint8_t cacheIndex, uint8_t oflags);
dir_t* readDirCache(); 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
}; };

View File

@ -328,7 +328,7 @@ void CardReader::printListing(
if (includeLongNames) { if (includeLongNames) {
SERIAL_CHAR(' '); SERIAL_CHAR(' ');
if (prependLong) { SERIAL_ECHO(prependLong); SERIAL_CHAR('/'); } if (prependLong) { SERIAL_ECHO(prependLong); SERIAL_CHAR('/'); }
SERIAL_ECHO(longFilename[0] ? longFilename : "???"); SERIAL_ECHO(longFilename[0] ? longFilename : filename);
} }
#endif #endif
SERIAL_EOL(); SERIAL_EOL();
@ -385,9 +385,9 @@ void CardReader::ls(
diveDir.rewind(); diveDir.rewind();
selectByName(diveDir, segment); selectByName(diveDir, segment);
// Print /LongNamePart to serial output // Print /LongNamePart to serial output or the short name if not available
SERIAL_CHAR('/'); SERIAL_CHAR('/');
SERIAL_ECHO(longFilename[0] ? longFilename : "???"); SERIAL_ECHO(longFilename[0] ? longFilename : filename);
// If the filename was printed then that's it // If the filename was printed then that's it
if (!flag.filenameIsDir) break; if (!flag.filenameIsDir) break;

View File

@ -84,21 +84,26 @@ def Upload(source, target, env):
#----------------# #----------------#
# File functions # # File functions #
#----------------# #----------------#
def _GetFirmwareFiles(): def _GetFirmwareFiles(UseLongFilenames):
if Debug: print('Get firmware files...') if Debug: print('Get firmware files...')
_Send('M20 F') _Send(f"M20 F{'L' if UseLongFilenames else ''}")
Responses = _Recv() Responses = _Recv()
if len(Responses) < 3 or not any('file list' in r for r in Responses): if len(Responses) < 3 or not any('file list' in r for r in Responses):
raise Exception('Error getting firmware files') raise Exception('Error getting firmware files')
if Debug: print('OK') if Debug: print('OK')
return Responses return Responses
def _FilterFirmwareFiles(FirmwareList): def _FilterFirmwareFiles(FirmwareList, UseLongFilenames):
Firmwares = [] Firmwares = []
for FWFile in FirmwareList: for FWFile in FirmwareList:
if not '/' in FWFile and '.BIN' in FWFile: # For long filenames take the 3rd column of the firmwares list
idx = FWFile.index('.BIN') if UseLongFilenames:
Firmwares.append(FWFile[:idx+4]) 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 return Firmwares
def _RemoveFirmwareFile(FirmwareFile): def _RemoveFirmwareFile(FirmwareFile):
@ -124,6 +129,8 @@ def Upload(source, target, env):
marlin_board_info_name = _GetMarlinEnv(MarlinEnv, 'BOARD_INFO_NAME') marlin_board_info_name = _GetMarlinEnv(MarlinEnv, 'BOARD_INFO_NAME')
marlin_board_custom_build_flags = _GetMarlinEnv(MarlinEnv, 'BOARD_CUSTOM_BUILD_FLAGS') marlin_board_custom_build_flags = _GetMarlinEnv(MarlinEnv, 'BOARD_CUSTOM_BUILD_FLAGS')
marlin_firmware_bin = _GetMarlinEnv(MarlinEnv, 'FIRMWARE_BIN') 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_custom_firmware_upload = _GetMarlinEnv(MarlinEnv, 'CUSTOM_FIRMWARE_UPLOAD') is not None
marlin_short_build_version = _GetMarlinEnv(MarlinEnv, 'SHORT_BUILD_VERSION') marlin_short_build_version = _GetMarlinEnv(MarlinEnv, 'SHORT_BUILD_VERSION')
marlin_string_config_h_author = _GetMarlinEnv(MarlinEnv, 'STRING_CONFIG_H_AUTHOR') marlin_string_config_h_author = _GetMarlinEnv(MarlinEnv, 'STRING_CONFIG_H_AUTHOR')
@ -148,6 +155,10 @@ def Upload(source, target, env):
# "upload_delete_old_bins": delete all *.bin files in the root of SD Card # "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', 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'] '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: try:
# Start upload job # Start upload job
@ -156,7 +167,7 @@ def Upload(source, target, env):
# Dump some debug info # Dump some debug info
if Debug: if Debug:
print('Upload using:') print('Upload using:')
print('---- Marlin --------------------') print('---- Marlin -----------------------------------')
print(f' PIOENV : {marlin_pioenv}') print(f' PIOENV : {marlin_pioenv}')
print(f' SHORT_BUILD_VERSION : {marlin_short_build_version}') print(f' SHORT_BUILD_VERSION : {marlin_short_build_version}')
print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}') print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}')
@ -164,8 +175,10 @@ def Upload(source, target, env):
print(f' BOARD_INFO_NAME : {marlin_board_info_name}') print(f' BOARD_INFO_NAME : {marlin_board_info_name}')
print(f' CUSTOM_BUILD_FLAGS : {marlin_board_custom_build_flags}') print(f' CUSTOM_BUILD_FLAGS : {marlin_board_custom_build_flags}')
print(f' FIRMWARE_BIN : {marlin_firmware_bin}') 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(f' CUSTOM_FIRMWARE_UPLOAD : {marlin_custom_firmware_upload}')
print('---- Upload parameters ---------') print('---- Upload parameters ------------------------')
print(f' Source : {upload_firmware_source_name}') print(f' Source : {upload_firmware_source_name}')
print(f' Target : {upload_firmware_target_name}') print(f' Target : {upload_firmware_target_name}')
print(f' Port : {upload_port} @ {upload_speed} baudrate') print(f' Port : {upload_port} @ {upload_speed} baudrate')
@ -175,9 +188,13 @@ def Upload(source, target, env):
print(f' Error ratio : {upload_error_ratio}') print(f' Error ratio : {upload_error_ratio}')
print(f' Test : {upload_test}') print(f' Test : {upload_test}')
print(f' Reset : {upload_reset}') print(f' Reset : {upload_reset}')
print('--------------------------------') print('-----------------------------------------------')
# Custom implementations based on board parameters # 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) # Delete all *.bin files on the root of SD Card (if flagged)
if upload_delete_old_bins: if upload_delete_old_bins:
@ -185,11 +202,6 @@ def Upload(source, target, env):
if not marlin_custom_firmware_upload: if not marlin_custom_firmware_upload:
raise Exception(f"CUSTOM_FIRMWARE_UPLOAD must be enabled in 'Configuration_adv.h' for '{marlin_motherboard}'") 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 # Init serial port
port = serial.Serial(upload_port, baudrate = upload_speed, write_timeout = 0, timeout = 0.1) port = serial.Serial(upload_port, baudrate = upload_speed, write_timeout = 0, timeout = 0.1)
port.reset_input_buffer() port.reset_input_buffer()
@ -198,13 +210,13 @@ def Upload(source, target, env):
_CheckSDCard() _CheckSDCard()
# Get firmware files # Get firmware files
FirmwareFiles = _GetFirmwareFiles() FirmwareFiles = _GetFirmwareFiles(marlin_long_filename_host_support)
if Debug: if Debug:
for FirmwareFile in FirmwareFiles: for FirmwareFile in FirmwareFiles:
print(f'Found: {FirmwareFile}') print(f'Found: {FirmwareFile}')
# Get all 1st level firmware files (to remove) # 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: if len(OldFirmwareFiles) == 0:
print('No old firmware files to delete') print('No old firmware files to delete')
else: else:

View File

@ -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 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 \ 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 \ 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 \ 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 \ EEPROM_SETTINGS EEPROM_CHITCHAT GCODE_MACROS CUSTOM_MENU_MAIN \
MULTI_NOZZLE_DUPLICATION CLASSIC_JERK LIN_ADVANCE QUICK_HOME \ MULTI_NOZZLE_DUPLICATION CLASSIC_JERK LIN_ADVANCE QUICK_HOME \