Recording analog input data stream and playing back for development

UPDATES:

This version uses a double buffer in order to not write as often to the SD card. It contains two libraries I wrote: SimpleSample and PlayerRecorder. With two libraries, we separate the sample rate library from the record/playback data library. This way, we can use the sample rate library when not recording/playing back data.

These are now c++ files because I switched to platform.io.

Filename issue in SD functions

There's an issue in the PlayerRecorder.cpp library. I had to hardcode the data filename into parts of the library. In the platform.io IDE, I couldn't use the variable _fileName for SD.remove("firsttry.txt") or SD.exists("firsttry.txt"). So you have to change that to your file name or figure out how to use the variable _fileName.

Using the _fileName in SD.remove(_fileName.c_str()) gave me this error:

argument of type "const char *" is incompatible with parameter of type "char *"C/C++(167)

player_recorder_example.cpp

#include <Arduino.h>
#include "../SimpleSample/SimpleSample.h"
#include <PlayerRecorder.h>
bool isRecording = true;                // false to play back recorded data, true to record
PlayerRecorder dataCorder(isRecording); // You set sample time to desired ms
SimpleSample sample(100);               // You set sample time to desired ms

void handleData(String data)
{
  Serial.print("handle data: ");
  Serial.println(data);
}

void setup()
{

  // Open serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial)
  {
    delay(1); // wait for serial port to connect. Needed for native USB port only
  }
  dataCorder.setUpFile("firsttry.txt");

  // =====WARNING: below is only for testing. Remove this code when recording real data.======
  // only deleteFille() during testing.
  // If you leave deleteFille() when recording real data you will delete
  // the data that you recorded as soon as you turn your arduino on again.
  if (isRecording)
    dataCorder.deleteFile();
  // =====================WARNING=================
}

void loop()
{

  //  checks if buffer needs to be emptied to the SD (RECORD) or filled with the SD (PLAY)
  dataCorder.checkBuffer();

  if (sample.isSampleTime())
  {
    String dataLine;
    if (isRecording)
    {
      int reading = analogRead(A0);
      int reading2 = analogRead(A1);
      dataLine = String(millis()) + "," + String(reading) + "," + String(reading2);
      //  RECORD
      dataCorder.record(dataLine);
    }
    else
    {
      //  PLAY
      if (dataCorder.endOfData)
        return;

      dataLine = dataCorder.play();
    }
    handleData(dataLine);
  }

  // Code below is used to stop recording in a testing environment
  // In the real world turning off the arduino could stop recording
  // But when you plug it back in it will start recording again,
  // so you may want to add a button to start the recording process and code that in as well
  if (isRecording && millis() > 3400)
  {
    dataCorder.close();  // without this a few of the last data points wont be saved
    dataCorder.dumpSD(); // optional if you want to see what your whole sd file looks like
    delay(10000000);
  }
}

PlayerRecorder.cpp

/*
  PlayerRecorder.cpp - keeps sample rate and handles recording and playing back data for the dream phone.
  Created by Dash Bark-Huss, Jan 28, 2022.
  Released into the public domain but you need to say a nice thing to stranger if you use it.

  Questions still not :
  1. How to close file when recording before unpluging with out loosing data? do you just let it run a bit? 
  Right not we call close() in the example after a certain time.
  2. Can't use _fileName in some areas *search "firsttry.txt" to see where

*/
// #ifndef ?
#include <SPI.h>
#include <SD.h>
#include "Arduino.h"
#include "PlayerRecorder.h"

PlayerRecorder::PlayerRecorder(bool record)
{

  _record = record;
  _sendData = false;
  _maxDataLineLength = 20; //(ex: 2800000,1000,1000\r\n\0) this should be max length of a data line plus null terminator (ex: 2800000,1000,1000\r\n\0)
  endOfData = false;
  _lastDataRecorded = false;
}

// ==============================================Record functions==========================================
// save data from sensor to buffer
// then save data to SD card

void PlayerRecorder::record(String dataString)
{

  _insertNextBufferDataLine(dataString);
};

void PlayerRecorder::_insertNextBufferDataLine(String dataString)
{
  // put data into input string
  // so that when full we send input string to sd card

  byte lastInd = 0;
  while (_inputString[lastInd] != '\0')
  {
    lastInd++;
  }

  byte ind = 0;
  while (dataString[ind] != '\0')
  {
    _inputString[lastInd + ind] = dataString[ind];
    ind++;
  }
  _inputString[lastInd + ind] = '\r';
  _inputString[lastInd + ind + 1] = '\n';
  _inputString[lastInd + ind + 2] = '\0';
}

bool PlayerRecorder::_dataBufferNeedsToSendToSD()
{
  byte index = 0;

  while (_inputString[index] != NULL && index < sizeof(_inputString))
  {
    index++;
  }

  if (index > sizeof(_inputString) - _maxDataLineLength)
  {
    return true;
  }
  return false;
}

void PlayerRecorder::_sendToSD()
{
  if (_sendData)
  {

    _myFile = SD.open(_fileName.c_str(), FILE_WRITE);

    _myFile.print(_inputString);
    _myFile.close();
    _arrayToNull(_inputString, sizeof(_inputString));
  }
}

void PlayerRecorder::deleteFile()
{
  Serial.println("deleting file.");

  // not working
  // SD.remove(_fileName.c_str());
  SD.remove("firsttry.txt");

  // not working
  // if (SD.exists(_fileName.c_str()))
  if (SD.exists("firsttry.txt"))
  {
    Serial.println("---ERROR DELETING----DATA.TXT exists.");
  }
}

// close _myFile manually
void PlayerRecorder::close()
{
  _myFile = SD.open(_fileName.c_str(), FILE_WRITE);

  _myFile.print(_inputString);
  _myFile.close();
  _arrayToNull(_inputString, sizeof(_inputString));
}

// ==============================================Playback functions==========================================
// // get data from SD card and save to buffer
// //

String PlayerRecorder::play()
{

  return _popNextBufferDataLine();
}

String PlayerRecorder::_popNextBufferDataLine()
{

  // put data line from inputString which is from
  //
  char data[_maxDataLineLength];
  _arrayToNull(data, sizeof(data));

  byte ind = 0;

  // save next line of _inputString to data
  while (_inputString[ind] != '\n' && _inputString[ind] != '\0')
  {
    data[ind] = _inputString[ind];
    ind++;
  }
  data[ind] = '\0';

  byte startMove = ind + 1;

  ind = 0;

  // clear line of _inputString that was saved to data
  while (ind < sizeof(_inputString))
  {
    char newChar = _inputString[ind + startMove];
    _inputString[ind] = newChar;
    ind++;
  }

  return data;
}

void PlayerRecorder::_saveFileToBuffer(byte maxChar, byte startFillingBufferFrom)
{
  byte numOfChar = 0 + startFillingBufferFrom;
  if (_myFile)
  {

    while (numOfChar < (maxChar - 1))
    {
      char newChar = _myFile.read();

      if (newChar == EOF)
      {
        _inputString[numOfChar] = '\0';
        _lastDataRecorded = true;
      }

      _inputString[numOfChar] = newChar;
      numOfChar++;
    }

    _inputString[maxChar - 1] = '\0';
  }
}

// PLAY find the first NULL character in _indexString if its lass than the next line taken
byte PlayerRecorder::_startPlace()
{
  // find the first NULL character in _indexString
  byte index = 0;

  while (_inputString[index] != NULL && index < sizeof(_inputString))
  {

    index++;
  }
  byte place = index;

  // if its less than the length of a data line
  if (index <= _maxDataLineLength)
  {

    return place;
  }

  return 0;
}

bool PlayerRecorder::_fileBufferNeedsToBeReset()
{
  byte index = 0;

  while (_inputString[index] != NULL && index < sizeof(_inputString))
  {

    index++;
  }

  // PLAY if its less than the length of a data line + 1
  // (or can be set to any number more than the length of a data line)  (maxdataline: 2800000,1000,1000\r\n\0 here null is at index 19)
  if (index <= _maxDataLineLength)
  {
    return true;
  }

  return false;
}

// ==============================================Shared functions==========================================

//  checks if buffer needs to be emptied to the SD (RECORD) or filled with the SD (PLAY)
void PlayerRecorder::checkBuffer()
{
  if (_record)
  {

    _sendData = _dataBufferNeedsToSendToSD();
    if (_sendData)
      _sendToSD();
  }
  else
  {
    if (endOfData)
      return;
    bool reset = _fileBufferNeedsToBeReset();

    if (_inputString[0] == NULL || reset || _inputString[0] == EOF)
    {
      if (_lastDataRecorded)
      {
        endOfData = true;
        Serial.println("PLAYBACK data ended-------");
        return;
      }

      byte bufStart = _startPlace();
      _saveFileToBuffer(sizeof(_inputString), bufStart);
    }
  }
}

void PlayerRecorder::_arrayToNull(char arr[], size_t sz)
{
  byte index = 0;
  while (index < 9)
  {
    arr[index] = '\0';
    index++;
  }
}

void PlayerRecorder::setUpFile(String fileName)
{
  _fileName = fileName.c_str();
  Serial.print("Initializing SD card...");
  // Open serial communications and wait for port to open:
  if (!Serial)
  {
    Serial.begin(115200);
    while (!Serial)
    {
    } // wait for serial port to connect. Needed for native USB port only
  }

  Serial.print("Initializing SD card...");
  if (!SD.begin(5))
  {
    Serial.println("initialization failed!");
    while (1)
      ;
  }
  Serial.println("initialization done.");

  if (_record)
  {
    // _deleteFile(); optional but may accidentally lead to deleted recently recorded data
    _myFile = SD.open(_fileName.c_str(), FILE_WRITE);
  }
  else
  {
    _myFile = SD.open(_fileName.c_str());
  }
  if (!_myFile)
  {
    // if the file didn't open, print an error:
    Serial.println("error opening file");
  }
}

// ==============================================Log functions==========================================

// prints all data in SD card file
void PlayerRecorder::dumpSD()
{
  _myFile = SD.open(_fileName.c_str());

  Serial.println("Printing entire SD file: ");
  if (_myFile)
  {
    while (_myFile.available())
    {
      Serial.write(_myFile.read());
    }
    _myFile.close();
  }
}

PlayerRecorder.h

/*
  PlayerRecorder.h - keeps sample rate and handles recording and playing back data for the dream phone.
  Created by Dash Bark-Huss, Jan 28, 2022.
  Released into the public domain but you need to say a nice thing to stranger if you use it.*/
#include <Arduino.h>
#ifndef PlayerRecorder_h
#define PlayerRecorder_h
#include <SPI.h>
#include <SD.h>

#include "Arduino.h"
class PlayerRecorder
{
public:
  PlayerRecorder(bool record);
  // Shared
  void checkBuffer();
  void setUpFile(String fileName);
  //record
  void record(String);
  void close();
  void deleteFile();
  // play
  String play();
  bool endOfData;
  // log
  void dumpSD();

private:
  // Shared
  int _BUF_DIM;
  bool _record;
  File _myFile;
  void _arrayToNull(char arr[], size_t sz);
  byte _maxDataLineLength;
  // record
  String _fileName;
  char _inputString[100];
  bool _dataBufferNeedsToSendToSD();
  void _sendToSD();
  void _insertNextBufferDataLine(String dataString);
  bool _sendData;
  // play
  String _popNextBufferDataLine();
  bool _fileBufferNeedsToBeReset();
  bool _lastDataRecorded;
  byte _startPlace();
  void _saveFileToBuffer(byte maxChar, byte startFillingBufferFrom);
};

#endif

SimpleSample.cpp

/*
  SimpleSample.cpp - keeps sample rate and handles recording and playing back data for the dream phone.
  Created by Dash Bark-Huss, Jan 28, 2022.
  Released into the public domain but you need to say a nice thing to stranger if you use it.
*/

#include "Arduino.h"
#include "SimpleSample.h"

SimpleSample::SimpleSample(long samplingDelta)
{
  currentSampleTime = 0;
  _samplingDelta = samplingDelta;
  _startTime = millis();
  _loops = 0;
}

bool SimpleSample::isSampleTime()
{
  unsigned long actualTime = millis();

  bool timeToSample = actualTime - _startTime >= _samplingDelta;
  if (timeToSample)
  {
    _loops = _loops + 1;
    currentSampleTime = actualTime;
    // currentSampleTime = _startTime + _samplingDelta

    _startTime = _startTime + _samplingDelta;
  }

  return timeToSample;
}

SimpleSample.h

/*
  SimpleSample.h - keeps sample rate and handles recording and playing back data for the dream phone.
  Created by Dash Bark-Huss, Jan 28, 2022.
  Released into the public domain but you need to say a nice thing to stranger if you use it.*/
#include <Arduino.h>
#ifndef SimpleSample_h
#define SimpleSample_h
#include <SPI.h>
#include <SD.h>

#include "Arduino.h"
class SimpleSample
{
public:
  SimpleSample(long samplingDelta);
  bool handleData;
  bool isSampleTime();
  unsigned long currentSampleTime;

private:
  unsigned long _samplingDelta;
  unsigned long _loops;
  unsigned long _startTime;
};

#endif

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