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