Alphanumeric sorting SD files using SdFat

Has anyone shared SdFat example code for sorting files on SD alphanumerically? I understand the FAT standard is based on indexes, and that the sorting would require an struct/array and a hefty amount of memory, but there are Arduino(-compatible) microcontrollers that can certainly handle it. I've looked at length for such an example and can't find any. I'm new enough to C that, while I might eventually figure it out, I would love some example code to get me started.

For my purposes, I need something like file.openNextAlphaNum() to open the next file in the sorted array, and loop back to the start at the end of the alphabet. Specifically, I actually need to collect the indexes of every folder in the root, and move into them sequentially in alphabetical order. The number of folders is unknown, but is likely to be around 100. I'm running on Teensy LC.

I understand there are tools that sort SD indexes, but I need a real-time solution so my customers can drop folders on an SD card without worrying about the index order.

Hi

Browse to my Arduino web server home automation application at http://www.2wg.co.nz/ and select the SD Card web page option on the left hand side menu.

What you immediately see is an alphabetically sorted list of the folders and files in the root directory of the application's SD card. (Directories/folders first, then files.)

Click on the PUBLIC folder name and the system will open up an alphabetical sorted list of files in the PUBLIC folder.

From the root directory/folder select the ACTIVITY folder name and the system will automatically drill down through several folder levels to the current month's daily activity log files. Also in alphabetical (numerical) order.

The full source code for my application (not including the last month of development that has not been released) is available in the SD Card \PUBLIC\ folder.

Within the file http://www.2wg.co.nz/public/HA150523.INO/ look for these two procedures:

  • SDCardListWebPage()
  • SDCardDirectoryBrowse()

Those two procedures are the implementation of the alphabetical SD Card directory/folder browsing web page sub-system.

In the SDCardDirectoryBrowse() procedure this code defines two small arrays used to sort folders and files for alphabetical listing on web pages.

  const int C_SortMax = 5;
  String l_SortList[C_SortMax]; //The sort array can be as small as one array element
  unsigned long l_FileSize[C_SortMax];

This implementation finds the first five folders/files alphabetically, then the next five, etc., etc. For every five folders/files the procedure is parsing the SD card directory/folder - so for example it does four parses of an SD card directory/folder if there are between 16 and 20 folders/files in the directory/folder.

C_SortMax can be as low as 1 in you have limited memory - the sorted listing would just take longer because more parses of the current directory/folder would be required.

My code is available for you to re-use as you wish. Note - I am using the <SD.h> library.

I hope you find it useful.

Cheers

Catweazle NZ

Thanks very much!

Very interesant but there is a 2 little problems here:

  1. The link is dead http://www.2wg.co.nz/public/HA150523.INO/

  2. I found this http://www.2wg.co.nz/PUBLIC/150918.ZIP

//----------------------------------------------------------------------------------------------

void SDCardListWebPage(const String &p_start_folder) { //Minimise Stack size
  const byte c_proc_num = 30;
  Push(c_proc_num);

  CheckRAM();
  ProcessCache(false); //Check cache size before SD file operations

  //See the SD card files /PUBLIC/INITWBPG.TXT and /PUBLIC/ACTNMENU.TXT
  //for the program code for these two common procedures.
  InitialiseWebPage(EPSR(E_SD_CARD_78)); //will install some javascript
  if (HTMLParmRead(E_HEAD_315) == "T") {
    CheckRAM();
    Pop(c_proc_num);
    return;
  }
  InsertActionMenu(600, EPSR(E_SD_CARD_78), St_SD_Card_List_WPE);

  G_EthernetClient.println(F("<tr style=\"background-color:Khaki;\">"));
  G_EthernetClient.println(F("<td><b>PAGE ACTIONS</b></td>"));
  G_EthernetClient.println(F("</tr>"));

  G_EthernetClient.print(F("<tr>"));
  G_EthernetClient.print(F("<td>")); 

  InsertLogFileLinks(0); //Hack (public) or all five links

  if ((G_SDCardOK) &&
      (UserLoggedIn())) {
    G_EthernetClient.print(F("
<input type='checkbox' name='FileDelete' value='Delete'>File Delete

"));

    G_EthernetClient.print(F("<input type='submit' formaction='/"));
    G_EthernetClient.print(G_PageNames[43]);
    G_EthernetClient.println(F("/' style='width:150px;height:30px;font-size:20px' value='File Upload'>"));
    G_EthernetClient.print(F("
"));
  }

  G_EthernetClient.print(F("</td>"));
  G_EthernetClient.print(F("</tr>"));

  CheckRAM();

  //END OF MASTER TABLE, ROW 1, CELL 1 on LHS
  //Time, Page Hits, Page Menu, Action Menu all on LHS
  G_EthernetClient.print(F("</table>"));  //LHS
  G_EthernetClient.print(F("</td>"));

  //START OF RHS PAGE DATA
  //A NEW CELL ON ROW 1 OF THE MASTER TABLE
  G_EthernetClient.println(F("<td style=\"vertical-align:top;\">"));
  G_EthernetClient.println(F("<table id='SDCardListTable' border=1>"));
  G_EthernetClient.print(F("<tr style=\"font-family:Courier;background-color:White;\">"));

  String l_parent_dir = p_start_folder;
  if (!G_SDCardOK) {
    ActivityWrite(EPSR(E_SD_Card_Failure_16));
    G_EthernetClient.print(F("<td>SD Card Failure</td>"));
  }
  else {
    if (l_parent_dir == "")
      l_parent_dir = "/";
    //
    //display the file if a local LAN user or if user logged in or is one of the public directories
    boolean l_SD_file_links = CheckFileAccessSecurity(l_parent_dir);
    String l_last_directory = "";
    int l_dir_count; //reset to zero within the sub procedure
    int l_file_count; //reset to zero within the sub procedure
    int l_panel_count = 0;
    while (true) {
      G_EthernetClient.println(F("<td style=\"vertical-align:top;\" align=\"left\">"));
      SDCardDirectoryBrowse(&l_parent_dir[0], l_SD_file_links, l_last_directory, l_dir_count, l_file_count);
      G_EthernetClient.println(F("</td>"));
      l_panel_count++;
      //Exit now if this is the root folder or there are no sub directories or files or if we already have four panels
      if ((p_start_folder == "/") || (l_dir_count == 0) || (l_file_count != 0) || (l_panel_count == 4)) {
        break;
      }
      //Otherwise we iterate to the last sub ddirectory/folder within this sub directory or folder.
      l_parent_dir = l_parent_dir + l_last_directory + '/';
    }
  } //SD Card OK - extract the file listing

  G_EthernetClient.println(F("</tr>"));

  G_EthernetClient.println(F("<tr><td style='background-color:sandybrown;'><b>CURRENT FOLDER:</b></td></tr>"));
  G_EthernetClient.print(F("<tr><td colspan=\"0\"><input type='text' name='FOLDER' style='font-family:Courier;font-size:20px;background-color:White;text-align:left;width:100%' "));
  G_EthernetClient.print(F("readonly value='"));
  G_EthernetClient.print(l_parent_dir);
  G_EthernetClient.print(F("'></td></tr>"));

  G_EthernetClient.println(F("<tr><td style='background-color:sandybrown;'><b>FILENAME:</b></td></tr>"));
  G_EthernetClient.print(F("<tr><td colspan=\"0\"><input type='text' id='FILENAME' name='FNAME' style='font-family:Courier;font-size:20px;background-color:White;text-align:left;width:100%' "));
  G_EthernetClient.print(F("value=''"));
  //G_EthernetClient.print(F("NULL"));
  G_EthernetClient.println(F("></td></tr>"));

  G_EthernetClient.println(F("</table>")); //RHS SDCardListTable
  G_EthernetClient.println(F("</td>"));

  G_EthernetClient.println(F("</tr>"));
  G_EthernetClient.print(F("</table>")); //form
  G_EthernetClient.println(F("</form>"));
  G_EthernetClient.println(F("</body></html>"));
  CheckRAM();
  Pop(c_proc_num);
} //SDCardListWebPage
void SDCardDirectoryBrowse(const String &p_parent_dir, boolean p_file_links, String &p_last_directory, int &p_dir_count, int &p_file_count) {
  //This procedure is called iteratively to auto traverse to the latest log files.
  const byte c_proc_num = 31;
  Push(c_proc_num);
  CheckRAM();
  SPIDeviceSelect(DC_SDCardSSPin);

  const char C_FileType_Dir  = '1';
  const char C_FileType_File = '2';

  File l_SD_root = SD.open(p_parent_dir.c_str(), FILE_READ);
  if (!l_SD_root) {
    ActivityWrite(Message(M_Unable_to_Open_SD_Card_Root_19));
    SPIDeviceSelect(DC_EthernetSSPin);
    Pop(c_proc_num);
    return;
  }

  //Using arrays we output a sorted directory listing
  //We only use 13 char strings so hopefully we get no String memory fragmentation
  const String C_StringMax = EPSR(E_13_tildes_109);
  const int C_SortMax = 5;
  String l_SortList[C_SortMax];	//The sort array can be as small as one array element
  unsigned long l_FileSize[C_SortMax];
  String l_PrevMax = "";
  int l_entry_count = 2; //Add one for the heading

  //Initialise the sort arrays
  for (byte l_idx = 0; l_idx < C_SortMax; l_idx++) {
    l_SortList[l_idx] = C_StringMax; //Reserves the filename space
    l_FileSize[l_idx] = 0;
  }
  CheckRAM();

  SPIDeviceSelect(DC_EthernetSSPin);
  G_EthernetClient.print(F("<table id='FileListTable' style=\"font-family:Courier;background-color:White;text-align:left;\">"));
  G_EthernetClient.println(F("<tr><td colspan=\"2\">")); 
  G_EthernetClient.println(F("<b>DIR: ")); 
  G_EthernetClient.print(p_parent_dir);
  G_EthernetClient.print(F("</b></td>")); 
  G_EthernetClient.print(F("</tr>"));
  G_EthernetClient.println(F("<tr><td><b>ENTRY NAME</b></td><td style=\"text-align:right;\"><b>SIZE</b></td></tr>"));
  SPIDeviceSelect(DC_SDCardSSPin);

  //We extract the SD card file list multiple times outputting a small sorted subset each time
  //SPIDeviceSelect(DC_SDCardSSPin);
  String l_SD_filename = "";
  boolean l_finished = false;
  boolean l_first    = true;
  File l_SD_file;
  p_last_directory = "";
  p_dir_count = 0;
  p_file_count = 0;
  CheckRAM();
  while (true) { //iterate until complete

    //In this first section we parse all the files in the directory and extract
    //the next C_SortMax set that is immediately greater that l_PrevMax.
    l_SD_root.rewindDirectory();
    while (true) {
      l_SD_file = l_SD_root.openNextFile();
      if (!l_SD_file) {
        break;
      }
      if (l_SD_file.isDirectory())
        l_SD_filename = String(C_FileType_Dir);
      else
        l_SD_filename = String(C_FileType_File);
      //
      l_SD_filename += l_SD_file.name();
      //Serial.println(l_SD_filename);
      if ((l_SD_filename > l_PrevMax) && (l_SD_filename < l_SortList[C_SortMax - 1])) {
        boolean l_inserted = false;
        //We are in range so we have to insert
        for (byte l_idx = C_SortMax - 1; l_idx > 0; l_idx--)  {
          //discard the final record (we can get it again on the next parse)
          l_SortList[l_idx] = l_SortList[l_idx - 1];
          l_FileSize[l_idx] = l_FileSize[l_idx - 1];
          //Now see if down one record is where to insert
          if (l_SD_filename > l_SortList[l_idx - 1]) {
            l_SortList[l_idx] = l_SD_filename;
            if (l_SD_file.isDirectory())
              l_FileSize[l_idx] = 0;
            else
              l_FileSize[l_idx] = l_SD_file.size();
            //
            l_inserted = true;
            break;
          } //if
        } //for
        if (!l_inserted) {
          //insert as first item - prev 1st iten was copied to be 2nd item
          l_SortList[0] = l_SD_filename;
          if (l_SD_file.isDirectory())
            l_FileSize[0] = 0;
          else
            l_FileSize[0] = l_SD_file.size();
          //
        } //if l_inserted
      } //if skip files already processed or beyond range
      l_SD_file.close();
    } //for each SD card file
    //We now have our sorted subset of SD card files

    CheckRAM();
    //Now print the array and reset its value
    //If the last cell of the array is C_SortMax we have finished
    SPIDeviceSelect(DC_EthernetSSPin);
    for (byte l_idx = 0; l_idx < C_SortMax; l_idx++) {
      if (l_SortList[l_idx] == C_StringMax) {
        l_finished = true;
        break; //we are done
      }
      if ((l_entry_count % 20) == 0) { //rollover to new column every 20 entries
        G_EthernetClient.print(F("</table>"));
        G_EthernetClient.print(F("</td>"));
        G_EthernetClient.println(F("<td style=\"vertical-align:top;\" align=\"left\">"));
        G_EthernetClient.print(F("<table style=\"font-family:Courier;background-color:White;text-align:left;\">"));
        G_EthernetClient.println(F("<tr><td><b>ENTRY NAME</b></td><td><b>SIZE</b></td>"));
        l_entry_count++; //for this heading
      }
      l_entry_count++; //for the file entry below

      G_EthernetClient.print(F("<tr><td>"));
      if ((p_file_links) || (l_SortList[l_idx][0] == C_FileType_Dir)) { //always show links for sub directories
        //Part I - Set up the html link
        G_EthernetClient.print(F("<a href=\""));
        G_EthernetClient.print(p_parent_dir);
        G_EthernetClient.print(l_SortList[l_idx].substring(1)); //file link pagename
        if (l_SortList[l_idx][0] == C_FileType_Dir) { //Directory
          //G_EthernetClient.print(F(".DIR/\">"));
          G_EthernetClient.print(F("/\">"));
        } //submit() never applies to directories
        else { //File
          //G_EthernetClient.print(F("/\" onclick='fnamesubmit(\""));
          G_EthernetClient.print(F("\" onclick='fnamesubmit(\""));
          G_EthernetClient.print(l_SortList[l_idx].substring(1));
          G_EthernetClient.print(F("\"); return false;'>"));
        }
      }

      //Part II - output the dir/file name
      G_EthernetClient.print(l_SortList[l_idx].substring(1)); //displayed file/dir name

      if ((p_file_links) || (l_SortList[l_idx][0] == C_FileType_Dir))  {//always show links for sub directories
        //Part III - terminate the html link
        G_EthernetClient.print(F("</a>"));
      }
      G_EthernetClient.print(F("</td>"));

      // files have sizes, directories do not
      G_EthernetClient.print(F("<td style=\"text-align:right;\">"));
      if (l_SortList[l_idx][0] == C_FileType_Dir) { //DIR
        p_dir_count++;
        p_last_directory = l_SortList[l_idx].substring(1);
        G_EthernetClient.print(F("[DIR]"));
      }
      else {
        p_file_count++;
        G_EthernetClient.print(l_FileSize[l_idx], DEC);
      }
      //
      G_EthernetClient.println(F("</td></tr>"));
      l_PrevMax = l_SortList[l_idx]; //record the updated prevMax
      l_SortList[l_idx] = C_StringMax; //Reset the array
      l_FileSize[l_idx] = 0; //Reset the array
    } //Print and reset each sort array record

    l_first = false;

    //We are finished if we encountered a C_StringMax record anywhere
    if (l_finished)
      break; //DC_EthernetSSPin already selected
    //
    SPIDeviceSelect(DC_SDCardSSPin);
  } //iterate until complete

  SPIDeviceSelect(DC_SDCardSSPin); //Just in case
  l_SD_root.close();

  SPIDeviceSelect(DC_EthernetSSPin);
  G_EthernetClient.println(F("<tr><td>"));
  G_EthernetClient.println(F("Subdirectories"));
  G_EthernetClient.println(F("</td><td style=\"text-align:right;\">"));
  G_EthernetClient.println(p_dir_count);
  G_EthernetClient.println(F("</td></tr>"));
  G_EthernetClient.println(F("<tr><td>"));
  G_EthernetClient.println(F("Files"));
  G_EthernetClient.println(F("</td><td style=\"text-align:right;\">"));
  G_EthernetClient.println(p_file_count);
  G_EthernetClient.println(F("</td></tr>"));
  G_EthernetClient.print(F("</table>")); //SDFileListTable
  CheckRAM();

  Pop(c_proc_num);
} //SDCardDirectoryBrowse