Read Serial array then plot it

It’s been a while since I played with Processing since OpenGL doesnt work, so I’m limited to sketches that don’t use P3D. The first idea I had is to read GPS through a Raspberry Pi Pico that uses the Arduino serial passthrough example. With my crippled Processing, I’m able to do some basic parsing and then print it.

Of course I want to do more than print Lat and Lon and want to plot azimuth and elevation for satellites in view. The sentence I’m parsing is the GPGSV which looks like this:

$GPGSV,2,1,08,02,74,042,45,04,18,190,36,07,67,279,42,12,29,323,36*77
$GPGSV,2,2,08,15,30,050,47,19,09,158,,26,12,281,40,27,38,173,41*7B

The first number is how many sentences, the next is the sentence number, then how many satellites, then the satellite number, then elevation, then azimuth. The rest is signal to noise for each satellite and then checksum.

Here’s the code I’m using to parse the sentence:
The initial code came from Tom Igoe and I changed it to parse the GPGSV.

void parseString(String serialString) {
  // split the string at the commas
  //and convert the sections into integers:
  String items[] = (split(serialString, ','));
  // if the first item in the sentence is our identifier, parse the rest:
  if (items[0].equals("$GPGSV") == true) {
    // move the items from the string into the variables:
    satelliteNum = int(items[4]);    elevation = int(items[5]);
    azimuth = int(items[6]);
    println("satellite: " + satelliteNum + " " + "altitude: " + elevation + " " + "azimuth: " + azimuth);
  }

Since I can’t use P3D, I’m thinking I could still plot it but how? I’m a bit rusty.

What’s confusing me is that I want each satellite to have it’s own plot, and there could be many but I’m only reading items[4], 5, and 6 but they are paired. Do I need a new array that gets appended by 3 and then read that?

The satellites dont move that fast, so there is a lot of repeat data. Here’s an example of how it changes after about 5 minutes.

satellite: 3 altitude: 33 azimuth: 247
satellite: 22 altitude: 28 azimuth: 135
satellite: 29 altitude: 13 azimuth: 40
satellite: 3 altitude: 33 azimuth: 247
satellite: 22 altitude: 28 azimuth: 135
satellite: 29 altitude: 13 azimuth: 40
satellite: 3 altitude: 33 azimuth: 247
satellite: 22 altitude: 28 azimuth: 135

There’s a new satellite in view:

satellite: 3 altitude: 30 azimuth: 242
satellite: 16 altitude: 78 azimuth: 212
satellite: 28 altitude: 23 azimuth: 85
satellite: 3 altitude: 30 azimuth: 242
satellite: 16 altitude: 78 azimuth: 212
satellite: 28 altitude: 23 azimuth: 85
satellite: 3 altitude: 30 azimuth: 242
satellite: 16 altitude: 78 azimuth: 212
satellite: 28 altitude: 23 azimuth: 85

It’s not clear to me what you want to visualise?

Little circles representing the position of the satellites?
x-axis elevation
y-axis azimuth
satellite number next to the sircle

And each time you get new data, plot in the same screen? Or wipe clean and draw new positions? Or do you want some kind of timebase on the x-axis?

I’m new to Processing so not sure if I can help. The above does not sound too difficult.

Thanks for the reply, anything helps.

What I had in mind is a continuous “line” for each satellite based on the azimuth and elevation. I know that I can use the map() function to fit both to the screen as x and y coordinates. like this:

if (azimuth < 360 && azimuth > 180) {
azimuth = (360 - azimuth);
azimuth = -180 + azimuth;
}
ycoord = map(elevation, 0, 90, ymin, ymax);
xcoord = map(azimuth, -180, 180, xmin, xmax);

Visually after many hours, I’d expect it to look something like this just smoother.

So I got some code started. What I notice is kinda neat, the points twinkle like a star. That’s not quite what I want even if the effect is cool. Here’s the code:

/* 
 GPS NMEA 0183 reader
 
 reads a GPS string in the NMEA 0183 format and returns lat/long. 
 
 For more on GPS and NMEA, see the NMEA FAQ:
 http://vancouver-webpages.com/peter/nmeafaq.txt
 
 by Tom Igoe
 created 1 June 2006
 */

import processing.serial.*;

int linefeed = 10;       // linefeed in ASCII
int carriageReturn = 13; // carriage return in ASCII
Serial myPort;           // The serial port
int sensorValue = 0;     // the value from the sensor

float latitude = 0.0;    // the latitude reading in degrees
String northSouth;       // north or south?
float longitude = 0.0;   // the longitude reading in degrees
String eastWest;         // east or west?

int sentences = 0;
int sentenceNum = 0;
int satellites = 0;
int satelliteNum = 0;
int elevation = 0;
int azimuth = 0;
float ycoord = 0;
float xcoord = 0;
int xmin = 0;
int xmax = 640;
int ymin = 0;
int ymax = 360;



void setup() {
  size(640, 360);
  
  // List all the available serial ports
  println(Serial.list());

  // I know that the first port in the serial list on my mac
  // is always my  Arduino, so I open Serial.list()[0].
  // Open whatever port is the one you're using.
  myPort = new Serial(this, Serial.list()[2], 9600);

  // read bytes into a buffer until you get a linefeed (ASCII 13):
  myPort.bufferUntil(carriageReturn);
}

void draw() {
  background(0);

// println(xcoord, ycoord);
 
   stroke(255);
  strokeWeight(4);
  beginShape(POINTS);
 // ycoord = map(float(elevation), float(0), float(90), float(ymin), float(ymax));
 // xcoord = map(float(azimuth), float(-180), float(180), float(xmin), float(xmax));
  vertex(xcoord, ycoord);

  endShape();

  }



//}
/*
  serialEvent  method is run automatically by the Processing applet
  whenever the buffer reaches the  byte value set in the bufferUntil() 
  method in the setup():
*/

void serialEvent(Serial myPort) { 
  // read the serial buffer:
  String myString = myPort.readStringUntil(linefeed);
  // if you got any bytes other than the linefeed:
  if (myString != null) {
    // parse the string:
    parseString(myString);
  }
} 

/*
  parseString takes the string and looks for the $GPGLL header.
  if it finds it, it splits the string into components at the commas,
  and stores them in appropriate variables.
*/

void parseString(String serialString) {
  // split the string at the commas
  //and convert the sections into integers:
  String items[] = (split(serialString, ','));
  // if the first item in the sentence is our identifier, parse the rest:
  //if (items[0].equals("$GPGLL") == true) {
  if (items[0].equals("$GPGSV") == true) {
    // move the items from the string into the variables:
    satelliteNum = int(items[4]);
    elevation = int(items[5]);
    azimuth = int(items[6]);
      
    if (azimuth <= 360 && azimuth > 180) {
    azimuth = 360 - azimuth;
    azimuth = -180 + azimuth;
    }

    ycoord = map(float(elevation), float(0), float(90), float(ymin), float(ymax));
    xcoord = map(float(azimuth), float(-180), float(180), float(xmin), float(xmax));

    println("satellite: " + satelliteNum + " " + "altitude: " + elevation + " " + "azimuth: " + azimuth);
  }
}

I took a screenshot. It only shows one point even though there were 4 satellite points plotted. I think it was the twinkle effect. What I’m looking to do is draw the points as they come in and not have the screen redraw them - I’m looking for persistance in how the points are drawn. I don’t know if that is possible without P3D??

Hello @HackinHarry,

This is some code that I cooked up to demonstrate simulating data to test your code without actually programming the Arduino:

/* 
 GPS NMEA 0183 reader
 
 reads a GPS string in the NMEA 0183 format and returns lat/long. 
 
 For more on GPS and NMEA, see the NMEA FAQ:
 http://vancouver-webpages.com/peter/nmeafaq.txt
 
 by Tom Igoe
 created 1 June 2006
 */

import processing.serial.*;

int linefeed = 10;       // linefeed in ASCII
int carriageReturn = 13; // carriage return in ASCII
Serial myPort;           // The serial port
int sensorValue = 0;     // the value from the sensor

float latitude = 0.0;    // the latitude reading in degrees
String northSouth;       // north or south?
float longitude = 0.0;   // the longitude reading in degrees
String eastWest;         // east or west?

int sentences = 0;
int sentenceNum = 0;
int satellites = 0;
int satelliteNum = 0;
int elevation = 0;
int azimuth = 0;
float ycoord = 0;
float xcoord = 0;
int xmin = 0;
int xmax = 640;
int ymin = 0;
int ymax = 360;

int xLast;
int yLast;

void setup() {
  size(640, 360);
  
  // List all the available serial ports
  println(Serial.list());

  // I know that the first port in the serial list on my mac
  // is always my  Arduino, so I open Serial.list()[0].
  // Open whatever port is the one you're using.
  myPort = new Serial(this, Serial.list()[2], 9600);

  //s read bytes into a buffer until you get a linefeed (ASCII 13):
  myPort.bufferUntil(carriageReturn);
  background(0);
  strokeWeight(1);
}

int sNum, elev, azim;

void draw() {
  //background(0);
  
  if (frameCount%10 ==0)
    {
    sNum+=10;
    elev = frameCount%90;
    azim = int(height/2 + 100*sin(frameCount*(TAU/360)));
    
    String sdata = "$GPGSV,2,1,08," + 
                    str(sNum) + "," + str(elev) + "," + str(azim) + "," +
                    "45,04,18,190,36,07,67,279,42,12,29,323,36*77" + '\n';
    serialEventSim(sdata);
    }

// println(xcoord, ycoord);
 
   stroke(255);
  strokeWeight(4);
 // beginShape(POINTS);
 //// ycoord = map(float(elevation), float(0), float(90), float(ymin), float(ymax));
 //// xcoord = map(float(azimuth), float(-180), float(180), float(xmin), float(xmax));
 // vertex(xcoord, ycoord);

 // endShape();
   stroke(255, 0, 0);
   point(xcoord%width, ycoord+5); // offset to visualize
   
  // Or:
  stroke(255, 255, 0);
  line(xLast, yLast, xcoord%width, ycoord);
  xLast =  int(xcoord%width);
  yLast =  int(ycoord);
   
   if(xcoord%width==0)
     background(0);
  }

//}
/*
  serialEvent  method is run automatically by the Processing applet
  whenever the buffer reaches the  byte value set in the bufferUntil() 
  method in the setup():
*/

void serialEventSim(String s) { 
  // read the serial buffer:
  //String myString = myPort.readStringUntil(linefeed);
  String myString = s;
  // if you got any bytes other than the linefeed:
  if (myString != null) {
    println(myString); // 0
    // parse the string:
    parseString(myString);
  }
} 

/*
  parseString takes the string and looks for the $GPGLL header.
  if it finds it, it splits the string into components at the commas,
  and stores them in appropriate variables.
*/

void parseString(String serialString) {
  // split the string at the commas
  //and convert the sections into integers:
  String items[] = (split(serialString, ','));
  // if the first item in the sentence is our identifier, parse the rest:
  //if (items[0].equals("$GPGLL") == true) {
  if (items[0].equals("$GPGSV") == true) {
    // move the items from the string into the variables:
    satelliteNum = int(items[4]);
    elevation = int(items[5]);
    azimuth = int(items[6]);
    println(satelliteNum, elevation, azimuth);
      
    //if (azimuth <= 360 && azimuth > 180) {
    //azimuth = 360 - azimuth;
    //azimuth = -180 + azimuth;
    //}

    //ycoord = map(float(elevation), float(0), float(90), float(ymin), float(ymax));
    //xcoord = map(float(azimuth), float(-180), float(180), float(xmin), float(xmax));
    
    xcoord = sNum;
    ycoord = azimuth;

    println("satellite: " + satelliteNum + " " + "altitude: " + elevation + " " + "azimuth: " + azimuth);
  }
}

Plot from above code:

image

The above is just for testing purposes and variables adjusted to plot something and that is all.
You may glean some insight that you can adapt to your code.

:)

1 Like

Thank you for helping clear some cobwebs. I think I might see what you did there. I’m going to let it run for a few hours and see what develops.

I let it run for several hours. I read some more on the $GPGSV sentence and each sentence gives azimuth and elevation for more than one satellite. I’m not sure where I got the wrong idea that each sentence only reported that for one satellite and the rest is signal information. I’ll need to figure out how to parse it more thoroughly.

After running for about 8 hours, it shows the trajectories for the satellites listed first. If there are 3 sentences, then it will plot 3 satellites. If 4, then it will plot 4 satellites but each sentence has more. To get what looks like a sin wave path will take days probably and there will probably be gaps until I change how it parses.

Just looking at it from a point of data collection, I would do the following

1 Create a class to store azimuth and elevation

class SatellitePosition
{
  int azimuth;
  int elevation;

  SatellitePosition(int azimuth, int elevation)
  {
    this.azimuth = azimuth;
    this.elevation = elevation;
  }
}

You can expand it with a constructor that takes Strings for azimuth and elevation and do the conversion to numeric values in that constructor.

2 Create a class for a satellite

class Satellite
{
  ArrayList<SatellitePosition> positions = new ArrayList<SatellitePosition>();
  color colour;
  static final int maxPositions = 360;

  // constructor for simulation
  Satellite(color colour)
  {
    this.colour = colour;
  }

// add a new received position
  void addPosition(int azimuth, int elevation)
  {
    if(positions.size() == maxPositions)
    {
      // remove first position from list
      positions.remove(0);
      
    }
    // add new position at end
    positions.add(new SatellitePosition(azimuth, elevation));
  }
}

The ArrayList acts like circular buffer. It starts filling it (I assume that there are 360 azimuth values) and once full it throws the first one away.

3 Create an ArrayList of Satellites

ArrayList<Satellite> satellites = new ArrayList<Satellite>();

If you receive new data, you parse it. If you find a new satellite in the parsed data, you add it to the satellites list. I’m not sure if a satellite can go ‘out of view’ in your received data; if it can, you can remove it from the satellites list by finding the index in that list and next removing it. Or you can start removing positions from the positions list for that satellite till there are none and next remove the satellite from the satellite list.

For each satellite that you find in the parsed data, you add the given azimuth and elevation to the positions list using the addPosition() method.

In Processing’s draw() method, you loop though the satellites list and for every satellite in that list you loop through the positions list and draw e.g. a dot…

Can you provide a file with e.g. 10 minutes of data or a few 100 lines?

According to NMEA-0183 message: GSV, you can have up to four satellites in one line. The sum of the satellites in the lines should be the number of satellites in the fourth field (index 3).

Parsing should not be difficult; possibly just a for-loop

1 Like

Wow, that’s good. Very helpful.

A txt file with some data is coming. The sketch I was running for hours and hours finally ran out of memory. I took a screenshot:

Alright, here you go. The file was too big to add as “code” and not an image format so you’ll need to go to this link:https://github.com/HarryDrones/NACA4Series/blob/master/GPGSV.txt

You could have zipped it and uploaded the zip :wink: But I got it :+1:

I came up with the following to parse a line that you received. I’m not sure how familiar you are with Processing but I would place the below in a new tab in your sketch; that will keep the ‘main’ sketch file clean. There are three classes

  1. An class for a custom exception
  2. A class for (the first part of) the data of the line
  3. A class for the details of the satellites in the line

Custom exception

//////////////////////////////////////////////////////////
// Custom exception class
//////////////////////////////////////////////////////////
class LineparserException extends Exception
{
  public LineparserException(String message)
  {
    super(message);
  }
}

Satellite details

//////////////////////////////////////////////////////////
// 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);
  }

  void print()
  {
    println("  satelliteNumber = " + str(satelliteNumber));
    println("  elevation = " + str(elevation));
    println("  azimuth = " + str(azimuth));
    println("  snr = " + str(snr));
  }
}

Based on the link that I provided earlier, the constructor is prepared to modify the PRN based on the messageID. I don’t know if you have a need for it but if you do you can adjust the PRNs in the if / else if. Note that the only thing that I know about satellites is bascially that they are flying around above our heads :wink:

LineParser

//////////////////////////////////////////////////////////
// class for satellite data
//////////////////////////////////////////////////////////
class LineParser
{
  String msgId;
  int numMessages;
  int msgNumber;
  int numSatellites;
  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 + "'");
    }

    String[] fields = data_cs[0].split(",");
    //printArray(fields);

    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] + "'");
      }
    }
  }

  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();
    }
  }
}

This class is the ‘main’ class for the parsing. You pass a received line to the contructor and it will handle the parsing. It handles the checksum validation. After parsing you have a record of the data of the line.

I’ve put your data in a file and used the following main file to test; it only processes the first 10 records

/*
process data file
 */

// for reading the file
BufferedReader reader;

void setup()
{
  selectInput("Select a file to process", "readFile");
}

void readFile(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
  } else {
    println("processing file '" + selection.getAbsolutePath() + "'");
    createRecords(selection.getAbsolutePath());
  }
}

/*
Read lines from file and parse
 In:
 full filename
 Returns:
 false on error, else true
 */
boolean createRecords(String filename)
{
  // return value
  boolean rb = true;
  // a line from the file
  String line = null;
  // a counter to keep track of the bnumber of lines with data
  int lineCnt = 0;

  try
  {
    reader = createReader(filename);

    // loop through the lines
    while ((line = reader.readLine()) != null)
    {
      // remove whitespace (just in case)
      line = line.trim();
      if (line.equals("") == false)
      {
        // parse the line
        LineParser lp = new LineParser(line);
        lp.print();

        lineCnt++;
        // bail out after 10 lines
        if (lineCnt == 10)
          break;
        ;
      }
    }
    reader.close();
  }
  catch (IOException e)
  {
    e.printStackTrace();
    rb = false;
  }
  catch(LineparserException e)
  {
    e.printStackTrace();
    rb = false;
  }

  if (rb == true)
  {
    println("There are " + str(lineCnt) + " effective lines");
  }

  return rb;
}

The result is below

processing file 'C:\Users\Wim\Documents\Processing\forum\42389_satellite\dataExample.1.txt'
msgId = 'GPGSV'
numMessages = 3
msgNumber = 2
numSatellites = 12
  satelliteNumber = 18
  elevation = 35
  azimuth = 183
  snr = 24
  satelliteNumber = 21
  elevation = 3
  azimuth = 327
  snr = -1
  satelliteNumber = 23
  elevation = 73
  azimuth = 77
  snr = 25
  satelliteNumber = 24
  elevation = 50
  azimuth = 61
  snr = 25
msgId = 'GPGSV'
numMessages = 3
msgNumber = 3
numSatellites = 12
  satelliteNumber = 25
  elevation = 3
  azimuth = 151
  snr = -1
  satelliteNumber = 27
  elevation = 6
  azimuth = 268
  snr = 8
  satelliteNumber = 28
  elevation = 6
  azimuth = 206
  snr = -1
  satelliteNumber = 32
  elevation = 37
  azimuth = 264
  snr = 29
msgId = 'GPGSV'
numMessages = 3
msgNumber = 1
numSatellites = 12
  satelliteNumber = 8
  elevation = 3
  azimuth = 300
  snr = -1
  satelliteNumber = 10
  elevation = 59
  azimuth = 321
  snr = 37
  satelliteNumber = 12
  elevation = 10
  azimuth = 115
  snr = 25
  satelliteNumber = 15
  elevation = 17
  azimuth = 69
  snr = 30
msgId = 'GPGSV'
numMessages = 3
msgNumber = 2
numSatellites = 12
  satelliteNumber = 18
  elevation = 35
  azimuth = 183
  snr = 24
  satelliteNumber = 21
  elevation = 3
  azimuth = 327
  snr = -1
  satelliteNumber = 23
  elevation = 73
  azimuth = 78
  snr = 32
  satelliteNumber = 24
  elevation = 50
  azimuth = 61
  snr = 29
msgId = 'GPGSV'
numMessages = 3
msgNumber = 3
numSatellites = 12
  satelliteNumber = 25
  elevation = 3
  azimuth = 151
  snr = -1
  satelliteNumber = 27
  elevation = 6
  azimuth = 268
  snr = 6
  satelliteNumber = 28
  elevation = 7
  azimuth = 206
  snr = -1
  satelliteNumber = 32
  elevation = 37
  azimuth = 264
  snr = 28
msgId = 'GPGSV'
numMessages = 3
msgNumber = 1
numSatellites = 12
  satelliteNumber = 8
  elevation = 3
  azimuth = 300
  snr = -1
  satelliteNumber = 10
  elevation = 59
  azimuth = 321
  snr = 35
  satelliteNumber = 12
  elevation = 10
  azimuth = 115
  snr = 24
  satelliteNumber = 15
  elevation = 17
  azimuth = 69
  snr = 30
msgId = 'GPGSV'
numMessages = 3
msgNumber = 2
numSatellites = 12
  satelliteNumber = 18
  elevation = 35
  azimuth = 183
  snr = 24
  satelliteNumber = 21
  elevation = 3
  azimuth = 327
  snr = -1
  satelliteNumber = 23
  elevation = 73
  azimuth = 78
  snr = 32
  satelliteNumber = 24
  elevation = 50
  azimuth = 61
  snr = 29
msgId = 'GPGSV'
numMessages = 3
msgNumber = 3
numSatellites = 12
  satelliteNumber = 25
  elevation = 3
  azimuth = 151
  snr = -1
  satelliteNumber = 27
  elevation = 6
  azimuth = 268
  snr = 6
  satelliteNumber = 28
  elevation = 7
  azimuth = 206
  snr = -1
  satelliteNumber = 32
  elevation = 37
  azimuth = 264
  snr = 27
msgId = 'GPGSV'
numMessages = 3
msgNumber = 1
numSatellites = 12
  satelliteNumber = 8
  elevation = 3
  azimuth = 300
  snr = -1
  satelliteNumber = 10
  elevation = 59
  azimuth = 321
  snr = 35
  satelliteNumber = 12
  elevation = 10
  azimuth = 115
  snr = 24
  satelliteNumber = 15
  elevation = 17
  azimuth = 69
  snr = 30
msgId = 'GPGSV'
numMessages = 3
msgNumber = 2
numSatellites = 12
  satelliteNumber = 18
  elevation = 35
  azimuth = 183
  snr = 24
  satelliteNumber = 21
  elevation = 3
  azimuth = 327
  snr = -1
  satelliteNumber = 23
  elevation = 73
  azimuth = 78
  snr = 33
  satelliteNumber = 24
  elevation = 50
  azimuth = 61
  snr = 29
There are 10 effective lines

How I visualise that you use it (note that this does not open a serial port, I’ll leave those details to you)


import processing.serial.*;

int linefeed = 10;       // linefeed in ASCII
int carriageReturn = 13; // carriage return in ASCII
Serial myPort;           // The serial port

// the parser
LineParser lp;


void setup()
{
}

void draw()
{
  // if a line was available
  if (lp != null)
  {
    for (int sdCnt = 0; sdCnt < lp.satData.size(); sdCnt++)
    {
      // plot each satellite; simulation by printing
      SatelliteData sd = lp.satData.get(sdCnt);
      println("  satelliteNumber = " + str(sd.satelliteNumber));
      println("  elevation = " + str(sd.elevation));
      println("  azimuth = " + str(sd.azimuth));
      println("  snr = " + str(sd.snr));
    }
    
    // the received data has been processed
    // destroy the data
    lp = null;
  }
}

void serialEvent(Serial myPort)
{
  // read a line
  String myString = myPort.readStringUntil(linefeed);
  // if you got any bytes other than the linefeed:
  if (myString != null)
  {
    try
    {
      lp = new LineParser(myString);
    }
    catch(LineparserException e)
    {
      e.printStackTrace();
    }
  }
}

This is not tested but it does not throw errors when I run it.

I don’t know how much of a programmer you are but don’t be tempted to modify the classes that I created to handle the drawing; the print methods in there are for debugging. Drawing should be separated from receiving and parsing. But looking at earlier code that you posted you are already doing that.

I think that that is it for now.

I appreciate your effort there. My programming is untrained, trial and error, interspersed with flashes of brilliance on rare occasion. The first time I ever sat at a computer was in high school and the computer was a mini computer in a different location. The first task was to login and enter a program using the keyboard and I couldnt understand any of it. I switched to wood shop and whittled a rainbow trout leaping out of the water to snatch a fly.

Years later I had one course in C++ so OOP is not completely foreign. Use it or lose it, so I’m shaking off the rust one line of code at a time. Thanks for your help. I’ll be back with what I do with this.

I found one issue while parsing the complete file

$GPGSV,3,1,12,08,03,299,,10,61,322,34,12,11,114,21,15,16,070,*73
             | sat 1    | sat 2      | sat 3      | sat 4   |

Satellite 4 seems to be missing a field; however the checksum is correct. A little research shows that the split method seems to throw away that last field if it’s empty. The fix is an additional parameter for the split method.

Revised LineParser class below

//////////////////////////////////////////////////////////
// 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] + "'");
      }
    }
  }

  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();
    }
  }
}

As additional check, the code now also checks if the number of fields is a multiple of 4 (there are 4 header fields and 4 fields per satellite) and throws an exception if not.

Regarding printing the exception. If you’re printing all the data, you might loose part of the exception that is printed with e.printStackTrace(); In my main file I’ve added a delay(1000) before the e.printStackTrace() to prevent that.

1 Like

That’s a ton to digest. I tried to get it to read the serial port and I’m doing something wrong with LineParser and how to instantiate that. I took a break with trying serial and thought I’d try the file reader. That file I saved is about 30 minutes worth of data.

A few hours of data and it’s looking closer to what I expected.

1 Like

You can play with the attached zip (OK, that did not work :frowning: ). 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

  1. 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
  1. 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
  1. port and baudrate are self-explaining
  2. outputDirectory specifies wher a logfile will be written; you’ll need to adjust it.
  3. 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.

  1. Quick blinks; could not access SD card
  2. Slower blinks; could not open file
  3. 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.

Console output

Opening serial port 'COM9'
Creating log file
C:\Users\Wim\Documents\Processing\forum\42389_satellite\log20230715204930.txt
syncing
DBG: Ready
synced
DBG: 5347
753	Adding satellite 18
753	Adding satellite 21
754	Adding satellite 23
754	Adding satellite 24
789	Adding satellite 25
789	Adding satellite 27
789	Adding satellite 28
789	Adding satellite 32
806	Adding satellite 8
806	Adding satellite 10
806	Adding satellite 12
806	Adding satellite 15
328663	Adding satellite 2
DBG: Done
Processing took 483 seconds
DBG: 134905

Note that I have not paid much attention to the displaying of the data (it’s just dots). You can change that in the function drawSatellites().

Please ask questions if needed. You need to understand how it works so you can maintain it yourself.

Hello @HackinHarry,

Can you please share one of your datasets (zip) for a longer duration?

I am generating some plots in 3D and this is some of the data plotted from your posted dataset:

image

I generated some data for testing but not realistic:

image

Code (minimal) for 3D plot:

// Plotting NMEA GPGSV data (azimuth and elevation) in 3D
// Author: glv
// Date:   2023-07-16

// Inspiration:
// https://discourse.processing.org/t/read-serial-array-then-plot-it/42389

// References:
// https://github.com/esutton/gps-nmea-log-files

String [] GPGSV; 
float sNum, el0, az0;

public void settings() 
  { 
  size(400, 400, P3D); 
  }
  
public void setup()
  {
  background(0);
  // Try one of these:
  //GPGSV = loadStrings("https://raw.githubusercontent.com/HarryDrones/NACA4Series/master/GPGSV.txt");
  GPGSV = loadStrings("https://raw.githubusercontent.com/esutton/gps-nmea-log-files/master/Random_NMEA_generator.txt");
  println(GPGSV[6]); 
  println(GPGSV.length);
  delay(2000); // To see above
  colorMode(HSB, 32, 100, 100);
  }
  
public void draw()
  {
  background(0);
  surface.setTitle(nf(int(frameRate)));
  lights();
  translate(width/2, 2*height/3, -200);
  
  rotateY(frameCount*(TAU/720));
  push();
  rotateX(TAU/4);
  stroke(5, 100, 100);
  circle(0, 0, 400);
  line(0, 0, 0, 0, 0, 200);
  pop();
  
  for (int i= 0; i<GPGSV.length; i+=1) // Increase as required.
    {
    String items[] = (split(GPGSV[i], ','));
    if (items[0].equals("$GPGSV") == true) 
      {
      sNum = PApplet.parseFloat(items[4]);
      el0 = PApplet.parseFloat(items[5]);
      az0 = PApplet.parseFloat(items[6]);
      println(sNum, az0, el0);
      strokeWeight(2);

      //3D plot using transformations
      push();
      rotateY(radians(az0));
      rotateZ(radians(-el0));
      stroke(sNum, 50, 100);
      point(150, 0, 0);
      pop();      
      }
    }      
  }    

With random data (link in code):

image

:)

1 Like