How to Use the SD Library In a Derived Class

Hello,

Microcontroller: Uno
Platform: PlatformIO / VSCode
Skill Level: Beginner + 50 hours of trail and error and Googling

Hopefully my terminology in the title makes sense and let me know if there's a better or more searchable title.

I'm attempting to make a class called CookBook which inherits from the "File" class in the Adafruit SD library. I'm stuck at the point where I need to open the file. If it wanted to do this in main.cpp it looks something like...

File file;

void setup()
{
  // Initialize the SD.
  if (!SD.begin(csPin)) {
    Serial.println(F("Could not initialize SD card. Try reinserting or restarting."));
    while(1);
  }

  // Open the file
  file = SD.open("CookBook.csv", FILE_READ);
  if (!file) {
    Serial.println(F("Could not open the Cook Book. Make sure it is named \"CookBook\" and is a .csv."));
    return;
  }
}

However, this can't be copy pasted into the Constructor of my CookBook class because when you get to this part...

file = SD.open("CookBook.csv", FILE_READ);

the "identifier "file" is undefined."

How can I open the file within the constructor of my CookBook class?

And bonus question if anyone is feeling helpful, what's happening under the hood with class inheritance? I think a File object is effectively being created when I create and object of my derived class, CookBook. And does an SD object also get created? Why do I have to do SD.begin(), but I can't do File.avalable()?

Thank you for any help. I really appreciate it.

Here's the entire CookBook.cpp file for the class. Note that it's not finished so there may be some weird comments in there.

#include <Arduino.h>
#include "CookBook.h"

CookBook::CookBook(byte csPin) : File()
{
    // Initialize the SD card
    if (!SD.begin(csPin))
    {
        Serial.println(F("Could not initialize the SD card."));
    }

    // Open the file
    file = SD.open("CookBook.csv", FILE_READ);
    if (!file)
    {
        Serial.println(F("Could not open the Cook Book. Make sure it is named \"CookBook\" and is a .csv."));
        return;
    }
}

//////////////////////////////////////////////////////////////////////////////////////////

bool CookBook::skipToLine(unsigned int lineNumber)
{
    for (unsigned int i=0; i<lineNumber; i++)
    {
        // Read until delimeter and place the cursor after it
        readStringUntil('\n');

        // Error check - Did we skip past all the data?
        if (available() == 0) 
        {
            Serial.println(F("Error: Skipped to the end of the file. Either no end line was found or the lineNumber > the number of lines"));
            return 0;
        }

    return 1;
    }
}

// //////////////////////////////////////////////////////////////////////////////////////////

unsigned int* CookBook::readIngredients(byte arraySize, unsigned int recipeIdx)
{
    unsigned int ingredientArray[arraySize];
    byte arrayIdx = 0; // Iterator for output ingredient array
    bool endl = false; // Flag to tell the loop to stop iterating if the end of a line is reached
    String element = ""; // String to store succesive characters until parsing
    char ch; // Temp variable to store individual characters read from the csv

    while (!endl) // loop until we hit the end of the line
    {
        // If we've reached the end of the file, parse then stop looping
        // Note: Check before reading the next character otherwise the loop will exit before parsing the last character
        if (available() == 0) {endl = true;}

        // Read the next character
        ch = read();
        // DEBUG // Serial.print("The current ch: "); Serial.print((int)ch); Serial.print(" and there are this many characters left to read: "); Serial.println(file.available());

        // If it's the end of a line or carrage return, parse then stop looping
        if (ch == '\n' || ch == '\r') {endl = true;}

        // Comma or end of the line... so we want to save or ignore it depending on what it is
        if (ch == ',' || endl)
        {
            // If the string to int conversion fails, it's not an int (it's a letter or something else)
            // Get rid of the string because we only want to read ingredients which are integers
            if (!element.toInt())
            {
                // DEBUG // Serial.println("Deleted the string because it wasn't just integers.");
                element = "";
            }

            // The string is made of integers
            else
            {
                // DEBUG // Serial.print("Saved a string as the fallowing int: "); Serial.println(element);
                ingredientArray[arrayIdx] = element.toInt(); // Store string as int
                element = ""; // Clear the string
                arrayIdx++;
            }
            
            continue; // Go to the next loop iteration without saving the escape character to the string
        }

        // Non-reasonable characters
        if (ch > 122 || ch < 48) // Less than ASCII "0" and greater than ASCII "z"
        {
            // DEBUG // Serial.println("Found an unreasonable character (not a number or letter). Skipping.");
        }

        // If no other criteria are met
        element += ch;
    }

    close();

    // Warning: Empty Array/Blank File
    if (ingredientArray[0] == 0) {Serial.println(F("Warning: The recipe seems to have no ingredients."));}

    return ingredientArray;
}

And here's the header file if it's relevant.

#ifndef CookBook_header
#define CookBook_header

#include <Arduino.h>
#include <SPI.h>
#include <SD.h>

class CookBook : private File
{
    public:
        // Constructor
        CookBook(byte csPin);

        unsigned int* readIngredients(byte arraySize, unsigned int recipeIdx);

        //byte* readRecipes();

    private:
        // Assumes a row, or line, ends with \n so fcn sets the cursor after "lineNumber" \n's
        // \input lineNumber - an integer value
        // \output - bool: 1 if successful, 0 if error
        bool skipToLine(unsigned int lineNumber);
};

#endif

do you feel confident that inheritance is the right way for your CookBook?

Would you formulate:

CookBook is a special version of File? --> Inheritance
or
CookBook uses a File? --> Composition

Hmmm... don't... I think.

Make a .begin() method and do it there. Give your class an attribute of type File.

Not sure you are or should be using inheritance. You are making a CookBook class that contains an File object.

Beginner at coding, or Arduino? Do you have other OO experience? If not, the concepts of OO can be confusing to a novice!

I don't feel confident in that. In fact, I didn't even know composition was a thing. Based on what you're saying, a composition is more appropriate.

@PaulRB, you're right. A begin method makes much more sense.

After googling composition vs inheritance here's the code that's changed...

From CookBook.cpp

CookBook::CookBook() {}

//////////////////////////////////////////////////////////////////////////////////////////

bool begin(byte csPin)
{
    // Initialize the SD card
    if (!SD.begin(csPin))
    {
        Serial.println(F("Could not initialize the SD card."));
        return 0;
    }

    // Open the file
    file = SD.open("CookBook.csv", FILE_READ);
    if (!file)
    {
        Serial.println(F("Could not open the Cook Book. Make sure it is named \"CookBook\" and is a .csv."));
        return 0;
    }

    return 1;
}

From CookBook.h:

class CookBook
{
    public:
        // Constructor
        CookBook();

        unsigned int* readIngredients(byte arraySize, unsigned int recipeIdx);

        bool begin(byte csPin);

        //byte* readRecipes();

    private:
        File file;
        
        // Assumes a row, or line, ends with \n so fcn sets the cursor after "lineNumber" \n's
        // \input lineNumber - an integer value
        // \output - bool: 1 if successful, 0 if error
        bool skipToLine(unsigned int lineNumber);

        byte _csPin;
};

I've added a private variable or "attribute,"as I think you're referring to it, and a new begin() method. The problem is "file" is still undefined within the begin() method when I try to do file = SD.open().

Before I moved this code to the begin() method I saw that it was defined within the constructor. Do I need to do something in the constructor so that the begin method can see "file?" Trying this->file doesn't seem to work though.

As far as my experience, I have similar levels of practice with Arduino and coding in general. I've just recently starting looking at OOP.

Your begin() function here is not part of the CookBook class, so it can't see the file attribute. You just made an ordinary C function that happens to be called begin() !

Take a look at the .cpp files in typical Arduino libraries and how the methods declared in the .h file are implemented in the .cpp files. Hint: "::"

I'm going to be really picky now...

bool begin(byte csPin)
{
    ....
        return 0;
    ...
    return 1;

A bool function should return true or false Not 0 and 1. Yes, I know that in C, false==0 and true is any non-zero value. C is "down and dirty" (doesn't have strong typing) but you don't have to be! You would not get away with that in many other languages. The Scala compiler for example would give you a whole page of complaints about that :wink:

You are declaring CookBook as a kind of File so instead of

    // Open the file
    file = SD.open("CookBook.csv", FILE_READ);

you should probably say:

    // Open the file aspect of this CookBook
    this = SD.open("CookBook.csv", FILE_READ);

Note: Since the 'File' part of CookBook is 'private' you can't do any File operations (like read) on a CookBook from outside the CookBook object.

Yep! In trying to quickly fix my code I forgot about the CookBook::begin().

After adjusting my code according to @noiasca and @PaulRB's suggestion to use composition rather than inheriting from the File class, adding a begin() method, and fixing the error @PaulRB pointed out with the not including CookBook::, everything works.

Here's my working code for anyone interested (I adjusted some other things at the same time, so hopefully that's not confusing).

CookBook.h

class CookBook
{
    public:
        // Constructor
        CookBook();

        void readRecipe(unsigned int(&array)[16]);

        bool begin(byte csPin);

        //byte* readRecipes();

    private:
        File file;

        // Assumes a row, or line, ends with \n so fcn sets the cursor after "lineNumber" \n's
        // \input lineNumber - an integer value
        // \output - bool: 1 if successful, 0 if error
        bool skipToLine(unsigned int lineNumber);
};

CookBook.cpp

CookBook::CookBook() {}

//////////////////////////////////////////////////////////////////////////////////////////

bool CookBook::begin(byte csPin)
{
    // Initialize the SD card
    if (!SD.begin(csPin))
    {
        Serial.println(F("Could not initialize the SD card."));
        return false;
    }

    // Open the file
    file = SD.open("CookBook.csv", FILE_READ);
    if (!file)
    {
        Serial.println(F("Could not open the Cook Book. Make sure it is named \"CookBook\" and is a .csv."));
        return false;
    }

    return true;
}

Thanks for the help everyone!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.