Retrieving numbers from an SD Card

I'm new to using an SD card, but after testing all the examples in the SD library, it seems pretty easy to access an SD card.

My SD card contains the file called: data.txt

This text file contains the following:

104.325,10.500 6.400,12.298 8.242,8.290 102.433,12.144

I need to read it, line by line, and get the 2 numbers into numbers I can manipulate and send to a motor controller. I’ve never worked with floats, and understand it’s not pretty doing math with them. My numbers on the text file will always have 3 decimal places, so I’m wondering how I could just read the number as a long, pretending the decimal point isn’t there? So the first line: 104.325,10.500 would give me 2 variables: long Xval = 104325 long Yval = 10500

Then I can easily do my math to use these numbers on a CNC table. Is there a way to parse the lines out like this? If this is not possible, I can have the numbers put on the SD card without the decimal point, but it will be easier on them if they can put the in there.

Yes, you can do that by simply removing the dot after you have read the line.

The below code demonstrates; it uses nul terminated character array (C strings, not C++ String with capital S)

void setup()
{
  Serial.begin(9600);

  // line from file
  char line[] = "104.325,10.500\r\n";

  // x 7 characters plus terminating nul character
  // adjust to your needs
  char x[8];
  // y 9 characters plus terminating nul character; caters for CR and/or LF
  // adjust to your needs
  char y[10];

  char *ptr;

  // get the comma
  ptr = strchr(line, ',');
  if (ptr == NULL)
  {
    Serial.println("Line does not contain comma");
    return;
  }

  // replace comma by nul character
  *ptr = '\0';
  // increment pointer to point to y
  ptr++;

  // line now only 'contains' x
  strcpy(x, line);
  // ptr now 'contains y
  strcpy(y, ptr);

  Serial.print("x = "); Serial.println(x);
  Serial.print("y = "); Serial.println(y);

  // find decimal dot in x
  ptr = strchr(x, '.');
  if (ptr == NULL)
  {
    Serial.println("x does not contain dot");
    return;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));
  Serial.print("new x = "); Serial.println(x);
  for (int cnt = 0; cnt < sizeof(x); cnt++)
  {
    Serial.println(x[cnt], HEX);
  }

  // find decimal dot in y
  ptr = strchr(y, '.');
  if (ptr == NULL)
  {
    Serial.println("y does not contain dot");
    return;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));
  Serial.print("new y = "); Serial.println(y);
  for (int cnt = 0; cnt < sizeof(y); cnt++)
  {
    Serial.println(y[cnt], HEX);
  }

  long xpos = atol(x);
  long ypos = atol(y);

  Serial.print("xpos = "); Serial.println(xpos);
  Serial.print("ypos = "); Serial.println(ypos);

}

void loop()
{
}

You can create a function based on the above to remove the decimal dot.

/*
  remove first dot from string
  returns false if no dot found, else true
*/
bool removeDot(char *value)
{
  // find decimal dot in value
  char *ptr = strchr(value, '.');
  if (ptr == NULL)
  {
    return false;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));

  return true;
}

And setup() changes to

void setup()
{
  Serial.begin(9600);

  // line from file
  char line[] = "104.325,10.500\r\n";

  // x 7 characters plus terminating nul character
  // adjust to your needs
  char x[8];
  // y 9 characters plus terminating nul character; caters for CR and/or LF
  // adjust to your needs
  char y[10];

  char *ptr;

  // get the comma
  ptr = strchr(line, ',');
  if (ptr == NULL)
  {
    Serial.println("Line does not contain comma");
    return;
  }

  // replace comma by nul character
  *ptr = '\0';
  // increment pointer to point to y
  ptr++;

  // line now only 'contains' x
  strcpy(x, line);
  // ptr now 'contains y
  strcpy(y, ptr);

  Serial.print("x = "); Serial.println(x);
  Serial.print("y = "); Serial.println(y);

  // remove decimal dot from x
  if (removeDot(x) == false)
  {
    Serial.println("x does not contain dot");
    return;
  }

  // remove decimal dot from y
  if (removeDot(y) == false)
  {
    Serial.println("y does not contain dot");
    return;
  }


  Serial.print("new y = "); Serial.println(y);
  for (int cnt = 0; cnt < sizeof(y); cnt++)
  {
    Serial.println(y[cnt], HEX);
  }

  long xpos = atol(x);
  long ypos = atol(y);

  Serial.print("xpos = "); Serial.println(xpos);
  Serial.print("ypos = "); Serial.println(ypos);

}

I hope that this gets you on track.

Note: the HEX display of x and y is there for debugging so one can really see what’s in the arrays.

Or just Parse the floats as they come in and multiply it by 1000L and store into an long int?

J-M-L: Or just Parse the floats as they come in and multiply it by 1000L and store into an long int?

But that will still come at the expense of float calculations; something OP wanted to prevent.

Sure. That's a way to understand it.

he said

I've never worked with floats, and understand it's not pretty doing math with them.

While there are limits to what float can do on an arduino because of the limited precision, a 3 digit float x 1000 is not going to create such a massive computing challenge and is simpler.

If I had to write it instead of playing with full buffers I would write a new specialParseFloat() function reading char by char, returning the first valid long integer number from the current position. Initial characters that are not digits (or the minus sign) are skipped as the dot in the middle and terminated by the first character that is not a floating point number that would avoid having to read lines and would be compatible with streams

sterretje:
Yes, you can do that by simply removing the dot after you have read the line.

The below code demonstrates; it uses nul terminated character array (C strings, not C++ String with capital S)

void setup()

{
  Serial.begin(9600);

// line from file
  char line = “104.325,10.500\r\n”;

// x 7 characters plus terminating nul character
  // adjust to your needs
  char x[8];
  // y 9 characters plus terminating nul character; caters for CR and/or LF
  // adjust to your needs
  char y[10];

char *ptr;

// get the comma
  ptr = strchr(line, ‘,’);
  if (ptr == NULL)
  {
    Serial.println(“Line does not contain comma”);
    return;
  }

// replace comma by nul character
  *ptr = ‘\0’;
  // increment pointer to point to y
  ptr++;

// line now only ‘contains’ x
  strcpy(x, line);
  // ptr now 'contains y
  strcpy(y, ptr);

Serial.print("x = "); Serial.println(x);
  Serial.print("y = "); Serial.println(y);

// find decimal dot in x
  ptr = strchr(x, ‘.’);
  if (ptr == NULL)
  {
    Serial.println(“x does not contain dot”);
    return;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));
  Serial.print("new x = "); Serial.println(x);
  for (int cnt = 0; cnt < sizeof(x); cnt++)
  {
    Serial.println(x[cnt], HEX);
  }

// find decimal dot in y
  ptr = strchr(y, ‘.’);
  if (ptr == NULL)
  {
    Serial.println(“y does not contain dot”);
    return;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));
  Serial.print("new y = "); Serial.println(y);
  for (int cnt = 0; cnt < sizeof(y); cnt++)
  {
    Serial.println(y[cnt], HEX);
  }

long xpos = atol(x);
  long ypos = atol(y);

Serial.print("xpos = "); Serial.println(xpos);
  Serial.print("ypos = "); Serial.println(ypos);

}

void loop()
{
}



You can create a function based on the above to remove the decimal dot.



/*
  remove first dot from string
  returns false if no dot found, else true
*/
bool removeDot(char *value)
{
  // find decimal dot in value
  char *ptr = strchr(value, ‘.’);
  if (ptr == NULL)
  {
    return false;
  }
  // move decimal part one position
  memmove(ptr, ptr + 1, strlen(ptr));

return true;
}



And setup() changes to


void setup()
{
  Serial.begin(9600);

// line from file
  char line = “104.325,10.500\r\n”;

// x 7 characters plus terminating nul character
  // adjust to your needs
  char x[8];
  // y 9 characters plus terminating nul character; caters for CR and/or LF
  // adjust to your needs
  char y[10];

char *ptr;

// get the comma
  ptr = strchr(line, ‘,’);
  if (ptr == NULL)
  {
    Serial.println(“Line does not contain comma”);
    return;
  }

// replace comma by nul character
  *ptr = ‘\0’;
  // increment pointer to point to y
  ptr++;

// line now only ‘contains’ x
  strcpy(x, line);
  // ptr now 'contains y
  strcpy(y, ptr);

Serial.print("x = "); Serial.println(x);
  Serial.print("y = "); Serial.println(y);

// remove decimal dot from x
  if (removeDot(x) == false)
  {
    Serial.println(“x does not contain dot”);
    return;
  }

// remove decimal dot from y
  if (removeDot(y) == false)
  {
    Serial.println(“y does not contain dot”);
    return;
  }

Serial.print("new y = "); Serial.println(y);
  for (int cnt = 0; cnt < sizeof(y); cnt++)
  {
    Serial.println(y[cnt], HEX);
  }

long xpos = atol(x);
  long ypos = atol(y);

Serial.print("xpos = "); Serial.println(xpos);
  Serial.print("ypos = "); Serial.println(ypos);

}



I hope that this gets you on track.

Note: the HEX display of x and y is there for debugging so one can really see what's in the arrays.

Thanks SO much. I was able to get it working great with this method.

I have gotten this working using Sterrettje's post. And my final code is this: To recap: the file on the SD card looked something like this 102.328,65.351 54.385,112.425 etc

And I needed to get the two (X & Y) values out of each line. Now, I am need to add one more piece of data to each line, another number. It would have up to 2 digits to the right of the decimal, and 3 digits to the left, as in the following example:

102.328,65.351,1.5 54.385,112.425,.125 etc

I don't understand how this code works well enough to make it get the third value. Could someone please help me with this. I don't normal just ask someone to 'do it for me' but I'm afraid it's too far over my head.

My working code that gets the first 2 digits is as follows:

  int currentHole = 0; //counter for the current hole being drilled
  dir = SD.open("/"); //open the main directory
  File dataFile = SD.open(fileNames[x]);

  if (dataFile) {   // if the file is available, read it:
    while (dataFile.available()) {
      char c = dataFile.read();
      if (c == '\n') {
        if (CharCounter > 0) {
          currentHole++; //add another hole
          if (currentHole >= startHole) { //it's okay to drill it
            char x[10]; //9 digits, longer than any size we'll have
            char y[10];
            char *ptr;
            ptr = strchr(line, ','); // get the comma
            if (ptr == NULL) {
              error_Code(15); //Line does not contain comma
            }
            *ptr = '\0'; // replace comma by nul character
            ptr++; // increment pointer to point to y
            // line now only 'contains' x
            strcpy(x, line);
            // ptr now 'contains y
            strcpy(y, ptr);
            //Serial.print(F("Now Drilling hole #")); Serial.println(currentHole);
            //Serial.print(F("x = ")); Serial.print(x); Serial.print("   ");
            //Serial.print(F("y = ")); Serial.println(y);
            ptr = strchr(x, '.'); // find decimal dot in x
            if (ptr == NULL) {
              error_Code(16); //x does not contain dot
            }
            // move decimal part one position
            memmove(ptr, ptr + 1, strlen(ptr));
            ptr = strchr(y, '.'); // find decimal dot in y
            if (ptr == NULL) {
              error_Code(17); //y does not contain dot
            }
            // move decimal part one position
            memmove(ptr, ptr + 1, strlen(ptr));
            long xpos = atol(x);
            long ypos = atol(y);
            //Serial.print(F("encoder x val = ")); Serial.print(xpos); Serial.print("   ");
            //Serial.print(F("encoder y val = ")); Serial.println(ypos);
            //SEND DRILL COMMAND(XPOS, YPOS)
            //There are 19,200 encoder pulses per inch
            //xpos & ypos are currently in 1000th's of an inch (ie 1000 is 1 inch)
            unsigned long M1val = xpos * 19200;
            unsigned long M2val = ypos * 19200;
            M1val = M1val / 1000;
            M2val = M2val / 1000;
            //Serial.print("Enocder val");
            //Serial.println(M1val);

Most people will suggest to use the strtok() function to split the line. The below will split on the comma.

void setup()
{
  char line[] = "102.328,65.351,1.5\r\n";

  char *token;

  /* get the first token */
  token = strtok(line, ",");

  /* walk through other tokens */
  while (token != NULL)
  {
    Serial.println(token);
    token = strtok(NULL, ",");
  }
  Serial.println("Done");
}

This is jjuust a slightly adapted version of C library function - strtok() - Tutorialspoint

There are some disadvantages to strtok; e.g. it will ignore empty tokens (e.g. “123.45,1.2” or “123.45,1.2,”) but one can work around them.

Now, I am need to add one more piece of data to each line, another number. It would have up to 2 digits to the right of the decimal, and 3 digits to the left, as in the following example:

102.328,65.351,1.5
54.385,112.425,.125
etc

This is inconsistent; you say that there are up to two decimals but you present three in the example?

Anyway, now you have split the line into tokens, you need to create a long value from each of them. The below function will take a nul terminated string (e.g. the token from above code) and convert it to a long.

Add the below to the beginning of your code; adjust to the number of decimals that is required.

// number of decimals
#define NUMDECIMALS 2

...
...

And the function (in a few pieces)

/*
  convert text to long
  returns:
    the converted value
*/
long convert2long(char *text)
{
  // work buffer for decimals; sufficient space to add nul terminator
  char decimals[NUMDECIMALS + 1];
  // converted value
  long value;

The function takes a nul terminated character array as input and returns the converted value. We first define a buffer to hold the decimals when we’re working with them and a variable to hold the conversion result.

  // find decimal dot in text
  char *ptr = strchr(text, '.');
  // no decimal dot
  if (ptr == NULL)
  {
    value = atol(text);
    for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
    {
      value *= 10;
    }
    return value;
  }

Next we find the decimal dot in the text; if it’s not found, it’s a number without decimals and we simply convert the text to long. The value is normalized (not sure if it’s the correct word) based on NUMDECIMALS.

For this, we have too multiply this with by 10 pow NUMDECIMALS; e.g. if NUMDECIMALS equals 2, we multiply by 100, if it’s 3 we multiply by 1000 and so on

If there was a decimal dot, this converts to the part before the decimals to long

  // replace decimal dot by nul character
  *ptr = '\0';
  // text is now truncated and only reflects the part before the decimal dot

  // convert
  value = atol(text);
  for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
  {
    value *= 10;
  }

The code first replaces the dot by a nul character and next converts.

Next we can process the decimals

  // make ptr point to the decimal part
  ptr++;
  // copy max NUMDECIMALS characters to decimals
  strncpy(decimals, ptr, NUMDECIMALS);
  // if number of characters in original is more than NUMDECIMALS, decimals will not contain a nul charater
  // make sure we have one
  decimals[sizeof(decimals) - 1] = '\0';

  // if number of decimals in the text is smaller than required number, add zeroes
  while (strlen(decimals) != sizeof(decimals) - 1)
  {
    strcat(decimals, "0");
  }

First the pointer is incremented so it now points to the decimal part. Next, up to NUMDECIMALS characters are copy from the decimal part in the original text to the variable decimals.
Next we will make sure that there is a terminating nul character. If the original text was e.g. “1.2345”, decimals will now contain 23 (if NUMDECIMALS equals 2) and there is no nul character. So we add one.
Next we pad till we have NUMDECIMALS decimals.

And lastly we will convert decimals to long and add it to the already existing value and return the converted value.

  value += atol(decimals);
  return value;
}

The full code to test this function

// number of decimals
#define NUMDECIMALS 2

void setup()
{
  Serial.begin(115200);
  char *x[] =
  {
    "",
    "3",
    "3000.25",
    ".1111",
    "2.3",
  };

  // convert texxt in x and display result
  for (int cnt = 0; cnt < 5; cnt++)
  {
    Serial.print("x = "); Serial.println(x[cnt]);
    long val = convert2long(x[cnt]);
    Serial.print("val = "); Serial.println(val);
  }
}

void loop()
{
}

/*
  convert text to long
  returns:
    the converted value
*/
long convert2long(char *text)
{
  // work buffer for decimals; sufficient space to add nul terminator
  char decimals[NUMDECIMALS + 1];
  // converted value
  long value;

  // find decimal dot in text
  char *ptr = strchr(text, '.');
  // no decimal dot
  if (ptr == NULL)
  {
    value = atol(text);
    for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
    {
      value *= 10;
    }
    return value;
  }

  // replace decimal dot by nul character
  *ptr = '\0';
  // text is now truncated and only reflects the part before the decimal dot
  value = atol(text);
  for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
  {
    value *= 10;
  }

  // make ptr point to the decimal part
  ptr++;
  // copy max NUMDECIMALS characters to decimals
  strncpy(decimals, ptr, NUMDECIMALS);
  // if number of characters in original is more than NUMDECIMALS, decimals will not contain a nul charater
  // make sure we have one
  decimals[sizeof(decimals) - 1] = '\0';

  // if number of decimals in the text is smaller than required number, add zeroes
  while (strlen(decimals) != sizeof(decimals) - 1)
  {
    strcat(decimals, "0");
  }

  value += atol(decimals);
  return value;
}

THANKS! I will try to digest this tomorrow. You explained it out where I think I can follow the logic of what's happening. I got my 'left of the decimal' and 'right of the decimal' mixed up! The number is INCHES, to the thousandths. and the largest is 18", so it could be ##.### at most.

And lastly combining the convert2long with the splitting of the record. Below has the two records that you showed (including CR and LF) and uses strtok().

Your converted values will be stored in an array of longs.

The code also demostrates how to get rid of CR and LF (in case that is needed).

// number of decimals
#define NUMDECIMALS 2

void setup()
{
  Serial.begin(115200);
  // lines in file
  char line1[] = "102.328,65.351,1.5\r\n";
  char line2[] = "54.385,112.425,.125\r\n";

  // a line
  char line[100];

  // the three values x, y and z
  long values[3];
  // initialize with an invalid value
  values[0] = values[1] = values[2] = 0x80000000;

  // retrieve the first line from file
  strcpy(line, line1);

  Serial.print("First record (not trimmed): '"); Serial.print(line); Serial.println("'");
  // trim the text to remove CR and LF
  while (line[strlen(line) - 1] == '\r' || line[strlen(line) - 1] == '\n')
  {
    line[strlen(line) - 1] = '\0';
  }
  Serial.print("First record (trimmed): '"); Serial.print(line); Serial.println("'");

  // pointer to token in record
  char *token;
  // index in values array where to store result of convert2long()
  byte index = 0;

  // get the first token
  token = strtok(line, ",");
  //walk through other tokens
  while (token != NULL)
  {
    Serial.println(token);
    values[index] = convert2long(token);
    Serial.println(values[index]);
    index++;
    // break when we have the three values; ignore rest of line
    if (index == sizeof(values) / sizeof(values[0]))
    {
      break;
    }
    token = strtok(NULL, ",");
  }

  // print the values
  Serial.print("x: "); Serial.println(values[0]);
  Serial.print("y: "); Serial.println(values[1]);
  Serial.print("z: "); Serial.println(values[2]);

  // demo to check if there were sufficient fields detected by strtok
  if (values[0] == 0x80000000 || values[1] == 0x80000000 || values[2] == 0x80000000)
  {
    Serial.println("Insufficient fields in record");
  }

  // retrieve next line
  strcpy(line, line2);
  index = 0;

  Serial.print("Second record (not trimmed): '"); Serial.print(line); Serial.println("'");
  // trim the text to remove CR and LF
  while (line[strlen(line) - 1] == '\r' || line[strlen(line) - 1] == '\n')
  {
    line[strlen(line) - 1] = '\0';
  }
  Serial.print("Second record (trimmed): '"); Serial.print(line); Serial.println("'");

  // get the first token
  token = strtok(line, ",");
  //walk through other tokens
  while (token != NULL)
  {
    Serial.println(token);
    values[index] = convert2long(token);
    Serial.println(values[index]);
    index++;
    // break when we have the three values; ignore rest of line
    if (index == sizeof(values) / sizeof(values[0]))
    {
      break;
    }
    token = strtok(NULL, ",");
  }

  // print the values
  Serial.print("x: "); Serial.println(values[0]);
  Serial.print("y: "); Serial.println(values[1]);
  Serial.print("z: "); Serial.println(values[2]);

  // demo to check if there were sufficient fields detected by strtok
  if (values[0] == 0x80000000 || values[1] == 0x80000000 || values[2] == 0x80000000)
  {
    Serial.println("Insufficient fields in record");
  }

  Serial.println("Done");

  return;
}

void loop()
{
}

/*
  convert text to long
  returns:
    the converted value
*/
long convert2long(char *text)
{
  // work buffer for decimals; sufficient space to add nul terminator
  char decimals[NUMDECIMALS + 1];
  // converted value
  long value;

  // find decimal dot in text
  char *ptr = strchr(text, '.');
  // no decimal dot
  if (ptr == NULL)
  {
    value = atol(text);
    for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
    {
      value *= 10;
    }
    return value;
  }

  // replace decimal dot by nul character
  *ptr = '\0';
  // text is now truncated and only reflects the part before the decimal dot
  value = atol(text);
  for (int cnt = 0; cnt < NUMDECIMALS; cnt++)
  {
    value *= 10;
  }

  // make ptr point to the decimal part
  ptr++;
  // copy max NUMDECIMALS characters to decimals
  strncpy(decimals, ptr, NUMDECIMALS);
  // if number of characters in original is more than NUMDECIMALS, decimals will not contain a nul charater
  // make sure we have one
  decimals[sizeof(decimals) - 1] = '\0';

  // if number of decimals in the text is smaller than required number, add zeroes
  while (strlen(decimals) != sizeof(decimals) - 1)
  {
    strcat(decimals, "0");
  }

  value += atol(decimals);
  return value;
}

Play with it, introduce some errors in lines that are ‘retrieved’ from the file and see what happens (e.g. leave a field empty). If you have questions, ask. At the end of the day, you are the one that needs to maintain the code.

The output of above

First record (not trimmed): '102.328,65.351,1.5
'
First record (trimmed): '102.328,65.351,1.5'
102.328
10232
65.351
6535
1.5
150
x: 10232
y: 6535
z: 150
Second record (not trimmed): '54.385,112.425,.125
'
Second record (trimmed): '54.385,112.425,.125'
54.385
5438
112.425
11242
.125
12
x: 5438
y: 11242
z: 12
Done

The strtok() function is really good about ignoring spaces in the data. So, why not make your data human-readable as well as computer-readable?

102.328, 65.351
54.385, 112.425

I’ve got this code working well for getting numbers off my SD card. But there’s one thing I’m needing to change.
I call the Convert2Long() function like this:

Xpos = convert2long(fileData[0]);

And it give Xpos a long value of the value containing the decimal point. (Which was actually just characters not numbers, in fileData[0])

But the Convert2Long function changes fileData[0], which didn’t matter at first, but now that I’ve added more options, for example, the user can pause a job, and then restart where they left off. In that case, I need fileData[0] to remain the same.

I see that the function is changing a pointer in fileData[0], so it can get the last half of the number (after the decimal), but I don’t know how to change the code so it it still does what it does, without changing fileData[0]

Sorry for such a long explanation of a little thing I need help with.
Here is the function:

#define NUMDECIMALS 3

long convert2long(char *text) {
  // work buffer for decimals; sufficient space to add nul terminator
  char decimals[NUMDECIMALS + 1];
  // converted value
  long value;

  // find decimal dot in text
  char *ptr = strchr(text, '.');
  // no decimal dot
  if (ptr == NULL) {
    value = atol(text);
    for (int cnt = 0; cnt < NUMDECIMALS; cnt++) {
      value *= 10;
    }
    return value;
  }

  // replace decimal dot by nul character
  *ptr = '\0';
  // text is now truncated and only reflects the part before the decimal dot
  value = atol(text);
  for (int cnt = 0; cnt < NUMDECIMALS; cnt++) {
    value *= 10;
  }

  // make ptr point to the decimal part
  ptr++;
  // copy max NUMDECIMALS characters to decimals
  strncpy(decimals, ptr, NUMDECIMALS);
  // if number of characters in original is more than NUMDECIMALS, decimals will not contain a nul charater
  // make sure we have one
  decimals[sizeof(decimals) - 1] = '\0';

  // if number of decimals in the text is smaller than required number, add zeroes
  while (strlen(decimals) != sizeof(decimals) - 1)
  {
    strcat(decimals, "0");
  }

  value += atol(decimals);
  return value;
}

You can make a copy of the original line and work on that copy instead of on the original line. You can use e.g. strdup()

char *workcopy = strdup(line[0]);
if(workcopy == NULL)
{
  Serial.println(F("Not enough memory"));
}
else
{
  Xpos = convert2long(workcopy);

  // once you're done, you need to free the memory !!
  free(workcopy);
}

Not tested

I don’t understand why you want to convert a float representation to a long, but atof() will convert the float representation to a float, which you can then assign to a long variable. That function does NOT modify the input string.

  // replace decimal dot by nul character
  *ptr = '\0';

THAT is what modified the input string.

atol() would have stopped at the decimal point, again without modifying the input string.