You can play with the attached zip (OK, that did not work ). It grew a bit out-of-hand when I started adding the serial. It ws not quiteb finished yet but it is working. I used a Sparkfun Pro Micro with SD card reader to playout the file.
You can configure the application in the Config tab
inputOption
selects between reading from file on the PC or from Serial. You can find the options in the Constants tab
- SERIAL, read from serial port
- FILE, read from file; the usual popup will show to select the file to read
outputOption
allows you to log read data (either from file or serial port) to a file
- NONE, do not write to log file
- RAW, write the RAW received data
- FORMATTED, write a formatted output
port
and baudrate
are self-explaining
outputDirectory
specifies wher a logfile will be written; you’ll need to adjust it.
timestampFormat
defines the timestamp that is added in the log file for each received message; selected format [yyyy/MM/dd hhmmss]
.
The filename of the log file is currently not configurable; it’s always logyyyyMMddhhmmss.txt
; the expected behaviour is that a new log file is created once a day at midnight but I did not test that.
The LineParser was adjusted to allow printing to log file.
The application sends a “?” to the Arduino and waits for a specific reply from the Arduino
DBG: Ready
As long as it does not get that reply, it will retry every 3 seconds. After that it will process the GPGSV messages.
Main sketch tab
import java.util.HashSet;
import processing.serial.*;
// for reading from file
BufferedReader reader;
// arduino serial
Serial arduinoSerial = null;
// enable sync
boolean syncEnable = true;
// indicate that we're in the process of syncing
boolean isSyncing = false;
// indicate that we are synced
boolean isSynced = false;
// how long to wait for a valid reply on sync
int syncTimeout = 3000;
// time that sync was sent
int syncStarttime;
// start time of processing
int transferStarttime;
// end time of processing
int transferEndtime;
// data received from serial port or file
String receivedData = null;
// for writing result to file
// taken from https://processing.org/reference/PrintWriter.html
PrintWriter pwLog = null;
// variable to collect unique satellite numbers
HashSet<Integer> satelliteNumbers = new HashSet<Integer>();
void setup()
{
size(720, 720);
background(0xFF000000);
processInputoption(inputOption);
}
void draw()
{
// process log option and open log file if needed
processLogoption(logOption);
// if a file is open
if (reader != null)
{
receivedData = readLineFromFile();
}
if (arduinoSerial != null)
{
// sync
sync();
// note: do not use serialEvent; it might set receivedData to null
readSerial();
}
// the line parser
LineParser lp = null;
// if something received (from serial or file)
if (receivedData != null)
{
//println("'" + receivedData + "'");
if (logOption == LogOptions.RAW)
{
writeLog(receivedData);
}
// parse it
lp = parseLine(receivedData);
// clear receivedData
receivedData = null;
}
// if parsing was successful
if (lp != null)
{
if (logOption == LogOptions.FORMATTED)
{
writeLog(lp);
}
// add satellites to hash set
for (int sCnt = 0; sCnt < lp.satData.size(); sCnt++)
{
if (satelliteNumbers.contains(lp.satData.get(sCnt).satelliteNumber) == false)
{
println(millis() + "\tAdding satellite " + lp.satData.get(sCnt).satelliteNumber);
}
satelliteNumbers.add(lp.satData.get(sCnt).satelliteNumber);
}
// draw the satellites
drawSatellites(lp);
}
}
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
/*
Read serial data (if available)
*/
void readSerial()
{
// if nothing to read
if (arduinoSerial.available() == 0)
{
return;
}
receivedData = arduinoSerial.readStringUntil(LF);
// if we got something
if (receivedData != null)
{
if (receivedData.startsWith("DBG:") == true)
{
print(receivedData);
}
// if we were syncing, stop syncing
if (isSyncing == true && receivedData.equals("DBG: Ready\n") == true)
{
println("synced");
isSynced = true;
isSyncing = false;
transferStarttime = millis();
} //
else if (isSynced == true && receivedData.equals("DBG: Done\n") == true)
{
transferEndtime = millis();
println("Processing took " + str((transferEndtime - transferStarttime) / 1000) + " seconds");
}
if (receivedData.startsWith("DBG: ") == true)
{
receivedData = null;
}
}
}
/*
Send sync message
*/
void sync()
{
// if sync is enabled and not in sync yet
if (syncEnable == true && isSynced == false)
{
if (firstRun == true || millis() - syncStarttime >= syncTimeout)
{
firstRun = false;
syncStarttime = millis();
if (isSynced == false)
{
// indicate that we're waiting for arduino to reply
println("syncing");
isSyncing = true;
// send sync
arduinoSerial.write("?");
}
}
} // end-of-if (syncEnable == true && isSynced == false)
}
/*
Draw the satellite positions
In:
parsed data
*/
void drawSatellites(LineParser data)
{
//stroke(0xFFC0C0C0);
//fill(0xFFC0C0C0);
for (int sdCnt = 0; sdCnt < data.satData.size(); sdCnt++)
{
SatelliteData sd = data.satData.get(sdCnt);
stroke(getColour(sd.satelliteNumber));
fill(getColour(sd.satelliteNumber));
ellipse(sd.azimuth * 2, height - 20 - (sd.elevation * 2), 2, 2);
}
}
/*
Parse line
In:
line to parse
Returns:
null on error, else lineparser opject
*/
LineParser parseLine(String line)
{
LineParser parser = null;
// remove whitespace (just in case)
line = line.trim();
if (line.equals("") == false)
{
try
{
// parse the line
parser = new LineParser(line);
}
catch(LineparserException e)
{
delay(1000);
e.printStackTrace();
}
}
return parser;
}
Config tab
/////////////////////////////////////////////
// User configuration
/////////////////////////////////////////////
/////////////////////////////////////////////
// Options
/////////////////////////////////////////////
// input option
final static InputOptions inputOption = InputOptions.SERIAL;
// output option where and how to save data
final static LogOptions logOption = LogOptions.RAW;
/////////////////////////////////////////////
// Serial
/////////////////////////////////////////////
String port = "COM9";
final static int baudrate = 115200;
// immediately do first sync
boolean firstRun = true;
/////////////////////////////////////////////
// Logging
/////////////////////////////////////////////
// output directory
final static String outputDirectory = "C:\\Users\\Wim\\Documents\\Processing\\forum\\42389_satellite\\";
// timestamp format
final static String timestampFormat = "[%04d/%02d/%02d %02d:%02d:%02d]";
Constants tab
/////////////////////////////////////////////
// Constants and enums
/////////////////////////////////////////////
/////////////////////////////////////////////
// Defined options
/////////////////////////////////////////////
/*
Input options
*/
private enum InputOptions
{
SERIAL,
FILE,
}
/*
output options
*/
private enum LogOptions
{
NONE,
RAW,
FORMATTED,
}
/////////////////////////////////////////////
// Defined colours
/////////////////////////////////////////////
/*
Some colours to choose from for drawing satellite trace
*/
final static color colours[] = {
0xFFFF0000,
0xFFFFFF00,
0xFF00FF00,
0xFF00FFFF,
0xFF0000FF,
0xFFFFFFFF,
0xFF800000,
0xFF808000,
0xFF800080,
0xFF008000,
0xFF008080,
0xFF000080,
0xFF808080,
};
/////////////////////////////////////////////
// Others
/////////////////////////////////////////////
final static char CR = 0x0D;
final static char LF = 0x0A;
FileIO tab
/////////////////////////////////////////////
// FileIO methods
/////////////////////////////////////////////
/////////////////////////////////////////////
// Logging
/////////////////////////////////////////////
String currentDate = "";
/*
Open log file
*/
void openLogfile()
{
// if the date changed
if (currentDate.equals(String.format("%04d%02d%02d", year(), month(), day())) == false)
{
println("Creating log file");
// if a log file was open, cleanup
if(pwLog != null)
{
pwLog.flush();
pwLog.close();
pwLog = null;
}
// remember the current data
currentDate = String.format("%04d%02d%02d", year(), month(), day());
// create a filename
String filename = outputDirectory + String.format("log%04d%02d%02d%02d%02d%02d.txt",
year(), month(), day(), hour(), minute(), second());
println(filename);
// and open it
pwLog = createWriter(filename);
}
}
/*
Write timestamp plus raw data to log file
In:
data to log
*/
void writeLog(String data)
{
if (pwLog != null)
{
// write formatted timestamp
pwLog.print(String.format(timestampFormat, year(), month(), day(), hour(), minute(), second()));
// append data
pwLog.println(data);
// always flush
pwLog.flush();
}
}
/*
Write timestamp plus formatted data to log file
In:
LineParser (data to log)
*/
void writeLog(LineParser data)
{
if (pwLog != null)
{
// write formatted timestamp
pwLog.print(String.format(timestampFormat, year(), month(), day(), hour(), minute(), second()));
// append data
data.print(pwLog);
// always flush
pwLog.flush();
}
}
/////////////////////////////////////////////
// Data file
/////////////////////////////////////////////
/*
Open data file
In:
selected file
*/
void openDataFile(File selection)
{
if (selection == null)
{
println("Window was closed or the user hit cancel.");
} //
else
{
// open file
reader = createReader(selection.getAbsolutePath());
}
}
/*
Read line from data file
Returns:
null on error or if no more lines to read, else line from file
*/
String readLineFromFile()
{
receivedData = null;
try
{
receivedData = reader.readLine();
// if there was nothing more to read
if (receivedData == null)
{
println("Closing file");
reader.close();
reader = null;
println("There are " + satelliteNumbers.size() + " satellite numbers:");
for (int sn : satelliteNumbers)
{
println(sn);
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
return receivedData;
}
Helpers tab
/////////////////////////////////////////////
// Helpers methods
/////////////////////////////////////////////
/*
Process output option; opens log file if needed
In:
configured option
*/
void processLogoption(LogOptions option)
{
switch(option)
{
case NONE:
// do nothing
break;
case RAW:
case FORMATTED:
openLogfile();
break;
}
}
/*
Process input option
In:
configured option
*/
void processInputoption(InputOptions option)
{
switch(option)
{
case FILE:
// prompt user to select a file
selectInput("Select a file to process", "openDataFile");
break;
case SERIAL:
try
{
// open serial port
println("Opening serial port '" + port + "'");
arduinoSerial = new Serial(this, port, baudrate);
}
catch(Exception e)
{
println(e.getMessage());
//e.printStackTrace();
}
break;
}
}
/*
Get a colour to use for drawing a satellite
In:
(satellite) number
Returns:
colour to use
*/
color getColour(int num)
{
return colours[num % colours.length];
}
LineParser tab
/////////////////////////////////////////////
// Line parser
/////////////////////////////////////////////
//////////////////////////////////////////////////////////
//Custom exception class
//////////////////////////////////////////////////////////
class LineparserException extends Exception
{
public LineparserException(String message)
{
super(message);
}
}
//////////////////////////////////////////////////////////
// class for satellite data
//////////////////////////////////////////////////////////
class SatelliteData
{
int satelliteNumber = -1;
int elevation = -1;
int azimuth = -1;
int snr = -1;
SatelliteData(String msgId, String satelliteNumber)
{
this.satelliteNumber = Integer.parseInt(satelliteNumber);
// adjust satellite number based on ID
if (msgId.equals("GPGSV") == true)
{
} //
else if (msgId.equals("GLGSV") == true)
{
} //
else if (msgId.equals("GLGSV") == true)
{
} //
else
{
// unsupported
}
}
void addDetails(String elevation, String azimuth, String snr)
{
if (elevation.equals("") == false)
this.elevation = Integer.parseInt(elevation);
if (azimuth.equals("") == false)
this.azimuth = Integer.parseInt(azimuth);
if (snr.equals("") == false)
this.snr = Integer.parseInt(snr);
}
/*
print results to file
*/
void print()
{
println(" satelliteNumber = " + str(satelliteNumber));
println(" elevation = " + str(elevation));
println(" azimuth = " + str(azimuth));
println(" snr = " + str(snr));
}
/*
print results to file
In:
PrintWriter (file)
*/
void print(PrintWriter pw)
{
pw.print(" satelliteNumber = " + str(satelliteNumber));
pw.print(" elevation = " + str(elevation));
pw.print(" azimuth = " + str(azimuth));
pw.println(" snr = " + str(snr));
}
}
//////////////////////////////////////////////////////////
// class for line parser
//////////////////////////////////////////////////////////
class LineParser
{
String msgId = "";
int numMessages = -1;
int msgNumber = -1;
int numSatellites = -1;
ArrayList<SatelliteData> satData = new ArrayList<SatelliteData>();
LineParser(String dataX)
throws LineparserException
{
//println("original data = '" + dataX + "'");
// throw away the start marker
dataX = dataX.substring(1);
// split in '*' to seperate actural data and checksum
String[] data_cs = dataX.split("\\*");
// we expect two strings (actual data and checksum)
if (data_cs.length != 2)
{
throw new LineparserException("Could not determine checksum in '" + dataX + "'");
}
// calculate the checksum
int cs = Integer.parseInt(data_cs[1], 16);
for (int cnt = 0; cnt < data_cs[0].length(); cnt++)
{
cs ^= data_cs[0].charAt(cnt);
}
// the result of the checksum calculation should be zero
if (cs != 0)
{
throw new LineparserException("Checksum error in '" + dataX + "'");
}
// bug fix; when a line ends with a comma (before the end marker), java does not consider it a filed
// see e.g. https://stackoverflow.com/questions/13939675/java-string-split-i-want-it-to-include-the-empty-strings-at-the-end
//String[] fields = data_cs[0].split(",");
String[] fields = data_cs[0].split(",", -1);
//printArray(fields);
// check number of fields; needs to be multiple of 4
if (fields.length % 4 != 0)
{
throw new LineparserException("Field count error in '" + dataX + "'");
//return;
}
msgId = fields[0];
numMessages = Integer.parseInt(fields[1]);
msgNumber = Integer.parseInt(fields[2]);
numSatellites = Integer.parseInt(fields[3]);
// process the remaining fields
for (int fCnt = 4; fCnt < fields.length; fCnt++)
{
SatelliteData sd = new SatelliteData(msgId, fields[fCnt]);
//println(sd.satelliteNumber);
//println("adding '" + fields[fCnt + 1] + "', '" + fields[fCnt + 2] + "', '" + fields[fCnt + 3] + "'");
sd.addDetails(fields[fCnt + 1], fields[fCnt + 2], fields[fCnt + 3]);
satData.add(sd);
// only add 3; for-loop will add one more
fCnt += 3;
if (fCnt + 1 == fields.length)
{
//println("no more data in received data");
} //
else
{
//println("next field will be '" + fields[fCnt + 1] + "'");
}
}
}
/*
print results to screen
*/
void print()
{
println("msgId = '" + msgId + "'");
println("numMessages = " + str(numMessages));
println("msgNumber = " + str(msgNumber));
println("numSatellites = " + str(numSatellites));
for (int sdCnt = 0; sdCnt < satData.size(); sdCnt++)
{
satData.get(sdCnt).print();
}
}
/*
print results to file
In:
PrintWriter (file)
*/
void print(PrintWriter pw)
{
pw.println("msgId = '" + msgId + "'");
pw.println("numMessages = " + str(numMessages));
pw.println("msgNumber = " + str(msgNumber));
pw.println("numSatellites = " + str(numSatellites));
for (int sdCnt = 0; sdCnt < satData.size(); sdCnt++)
{
satData.get(sdCnt).print(pw);
}
}
}
This is the Arduino code that I used
/*
This sketch reads NMEA sentencences from SD card and sends it out over serial
*/
#include <SdFat.h>
// SD card chip select pin. Adjusted for ProMicro setup with CS on A0
const uint8_t chipSelect = A0;
// file system object.
SdFat sd;
// Use for file creation in folders.
SdFile file;
char filename[13] = "data.4.txt";
// interval between sends
const uint32_t interval = 1UL;
uint32_t lastActiontime = 0;
// blink for errors
const uint8_t RXLED = 17; // The RX LED has a defined Arduino pin
// indicate if we received a sync request ('?') from the terminal program
bool isSynced = false;
void setup()
{
Serial.begin(115200);
while (!Serial) {}
if (!sd.begin(chipSelect, SD_SCK_MHZ(50)))
{
pinMode(RXLED, OUTPUT);
Serial.println(F("DBG: Could not start SD"));
for (;;)
{
blink(150);
}
}
if (!file.open(filename, O_RDONLY))
{
pinMode(RXLED, OUTPUT);
Serial.print(F("DBG: Could not open file '"));
Serial.print(filename);
Serial.println(F("'"));
for (;;)
{
blink(300);
}
}
}
void blink(uint32_t delayTime)
{
static uint8_t ledStatus = LOW;
if (millis() - lastActiontime >= delayTime)
{
lastActiontime = millis();
ledStatus = !ledStatus;
digitalWrite(RXLED, ledStatus);
}
}
void loop()
{
if (Serial.available())
{
char ch = Serial.read();
if (ch == '?')
{
Serial.print(F("DBG: Ready\n"));
Serial.print(F("DBG: "));
Serial.print(millis());
Serial.print(F("\n"));
isSynced = true;
}
}
if (isSynced == false)
{
return;
}
char line[256];
memset(line, '\0', sizeof(line));
if (file.isOpen())
{
if (file.available())
{
if (millis() - lastActiontime >= interval)
{
//lastActiontime = millis();
char lf[] = "\n";
file.fgets(line, sizeof(line), lf);
Serial.print(line);
}
}
else
{
file.close();
Serial.print(F("DBG: Done\n"));
Serial.print(F("DBG: "));
Serial.print(millis());
Serial.print(F("\n"));
}
}
if (file.isOpen() == false)
{
// indicate that file played out
blink(750);
}
}
The first part of loop()
checks if thesync character was received; you can implement the same in your Arduino code. I’m planning to make the syncing optional but did not get there yet.
The code uses three status indications; I doubt that they are relevant in your scenario. Note that the Pro Micro does not have a built-in LED and hence the abuse of the RX LED.
- Quick blinks; could not access SD card
- Slower blinks; could not open file
- Slow blinks; file playout finished.
Once the status is displayed, you will have to reset the Arduino and restart the Processing application for a new run. For me that was easier and partially needed becase of the fle approach (I don’t have a GPS).
Note:
It takes the Arduino 130 seconds to send the file, it takes my old PC (2nd generation I3) 8 minutes to display the data.