devinaconley / arduino-plotter

An Arduino library for easy graphing on host computer via serial communication
MIT License
186 stars 31 forks source link

enhancement / recording data to text file for further analysis #33

Closed phdv61 closed 4 years ago

phdv61 commented 4 years ago

Hello @devinaconley I modified your "listener" source code attached below to be able, by pressing the space key, to toggle time-stamped recording of all variables on hard disk, in a date&time-stamped CSV - text file. first line gives the names of the variables stored in the file. rgds

/*

This listener is the main processing script that corresponds to the Arduino Plotter library for Arduino. This driver script handles serial port information and manages a set of Graph objects to do the actual plotting.

The library stores and handles all relevant graph information and variable references, and transfers information via the serial port to a listener program written with the software provided by Processing. No modification is needed to this program; graph placement, axis-scaling, etc. are handled automatically. Multiple options for this listener are available including stand-alone applications as well as the source Processing script.

The library, these listeners, a quick-start guide, documentation, and usage examples are available at:

https://github.com/devinaconley/arduino-plotter


Arduino Plotter Listener Modified PhDV61 : On-off recording in a text file for excel off-line analysis) v2.2.1 https://github.com/devinaconley/arduino-plotter by Devin Conley

*/

import processing.serial.*; import java.util.Map;

import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException;

// FLAG FOR DEBUG MODE final boolean DEBUG = false;

//CONSTANTS final char OUTER_KEY = '#'; final int MARGIN_SZ = 20; // between plots final int BG_COL = 75; // background final int PORT_INTERVAL = 5000; // time to sit on each port final int CONNECT_TIMEOUT = 2000; // force timeout on connecting to serial port final int BAUD_RATE = 115200;

final HashMap<String, Integer> COLORMAP = new HashMap<String, Integer>() { { put( "red", color( 255, 0, 0 ) ); put( "green", color( 0, 255, 0 ) ); put( "blue", color( 0, 0, 255 ) ); put( "orange", color( 255, 153, 51 ) ); put( "yellow", color( 255, 255, 0 ) ); put( "pink", color( 255, 51, 204 ) ); put( "purple", color( 172, 0, 230 ) ); put( "cyan", color( 0, 255, 255 ) ); } };

// Setup and config Globals int h; int w; int numGraphs; String configCode = "This will not be matched!"; String lastLabels = "Also will not be matched"; boolean configured = false; int lastConfig; int lastPortSwitch; int portIndex; Serial port; ArrayList graphs;

// variables to create file name, and manage recording ( see keypressed at the bottom ). The space bar will toggle ON-OFF simultaneous recording in a text file during graph display PrintWriter output; int d = day(); // Values from 1 - 31 int m = month(); // Values from 1 - 12 int y = year(); // 2003, 2004, 2005, etc. int sec = second(); // Values from 0 - 59 int min = minute(); // Values from 0 - 59 int hr = hour(); // Values from 0 - 23 boolean Recording=false; // We do NOT record initially. Only pressing the space bar will toggle recording. Esc to get out will flush the outstanding data to file.

void setup() { // Canvas size( 1430, 830 ); surface.setResizable( true ); h = height; w = width; frameRate(20); // 20 (50ms) instead of 50 frames/s = 4ms

// Serial comms
while ( Serial.list().length < 1 )
{
text( "No serial ports available. Waiting...", 20, 20 );    
delay( 100 );
}
portIndex = 0;
lastPortSwitch = millis();
attemptConnect( portIndex );

// creating a "time-stamped" file name  : "listeneryyyy-mm-dd hh.mm.ss.txt" including a full date to be able to recover files easilyt later...
String sd = str(y) + "-" + nf(m,2) + "-" + nf(d,2) + " " + nf(hr,2) + "." + nf(min,2) + "." + nf(sec,2);
output = createWriter("listener"+sd+".txt"); 
output.print(" time(ms), "); // Write the legend to the first line of text file

}

void draw() { //PLOT ALL try { background( BG_COL );

if ( configured )
{       
    for( int i = 0; i < graphs.size(); i++ )
    {
    graphs.get(i).Plot();
    }
}
else
{
  // Continue to scan ports if not configuring
    text( "Scanning serial ports... (" + Serial.list()[portIndex] + ")", 20, 20 );

    if ( millis() - lastPortSwitch > PORT_INTERVAL )
    {   // Go to next port      
    portIndex++;
    if ( portIndex >= Serial.list().length )
    {
        portIndex = 0;
    }

    logMessage( "Trying next port... index: " + portIndex + ", name: " + Serial.list()[portIndex],
            true );

    attemptConnect( portIndex );
    }
}
// Resize if needed
if ( h != height || w != width)
{
    h = height;
    w = width;
    float[][] posGraphs = setupGraphPosition( numGraphs );
    for ( int i = 0; i < numGraphs; i++ )
    {
    graphs.get(i).Reconfigure( posGraphs[i][0], posGraphs[i][1], posGraphs[i][2], posGraphs[i][3] );
    }
}
}
catch ( Exception e )
{}

}

void serialEvent( Serial ser ) { // Listen for serial data until #, the end of transmission key try { String message = ser.readStringUntil( OUTER_KEY ); if ( message == null || message.isEmpty() || message.equals( OUTER_KEY ) ) { return; }

JSONObject json = parseJSONObject( message );

if ( json == null )
{
    return;
}       

// ********************************************************* //
// ************* PLOT SETUP FROM CONFIG CODE *************** //
// ********************************************************* //

String tempCode = "";
boolean config = false;
if ( json.hasKey( "ng" ) && json.hasKey( "lu" ) )
{
    tempCode = Integer.toString( json.getInt( "ng" ) ) + Integer.toString( json.getInt( "lu" ) );
    config = true;
}

// If config code has changed, need to go through setup again
if ( config && !configCode.equals( tempCode ) )
{
    lastPortSwitch = millis(); // (likely on the right port, just need to reconfigure graph layout)

    // Check for size of full transmission against expected to flag bad transmission
    numGraphs = json.getInt( "ng" );

    JSONArray jsonGraphs = json.getJSONArray( "g" );

    if ( jsonGraphs.size() != numGraphs )
    {
    return;
    }

    configured = false;
    String concatLabels = "";

    // Setup new layout
    float[][] posGraphs = setupGraphPosition( numGraphs );

    graphs = new ArrayList<Graph>();

    // Iterate through the individual graph data blocks to get graph specific info
    for ( int i = 0; i < numGraphs; i++ )
    {
    JSONObject g = jsonGraphs.getJSONObject( i );

    String title = g.getString( "t" );
    boolean xvyTemp = g.getInt( "xvy" ) == 1;
    int maxPoints = g.getInt( "pd" );
    int numVars = g.getInt( "sz" );
    String[] labelsTemp = new String[numVars];
    int[] colorsTemp = new int[numVars];

    concatLabels += title;

    JSONArray l = g.getJSONArray( "l" );
    JSONArray c = g.getJSONArray( "c" );

    for ( int j = 0; j < numVars; j++ )
    {   
        labelsTemp[j] = l.getString( j );
    output.print(labelsTemp[j]); // Write the legend to the first line of text file
    if (j < numVars-1)  output.print(", ");
        colorsTemp[j] = COLORMAP.get( c.getString( j ) );
        if ( colorsTemp[j] == 0 )
        {
        logMessage( "Invalid color: " + c.getString( j ) + ", defaulting to green.", true );
        colorsTemp[j] = COLORMAP.get( "green" );
        }
        concatLabels += labelsTemp[j];
    }

output.println("");

    if ( xvyTemp )
    {
        numVars = 1;
    }

    // Create new Graph
    Graph temp = new Graph( this, posGraphs[i][0], posGraphs[i][1], posGraphs[i][2], posGraphs[i][3],
                xvyTemp, numVars, maxPoints, title, labelsTemp, colorsTemp );
    graphs.add( temp );
    }

    // Set new config code
    if ( concatLabels.equals( lastLabels ) ) // Only when we're sure on labels
    {
    configCode = tempCode;
    lastConfig = millis();
    logMessage( "Configured " + graphs.size() + " graphs", false ); 
    }
    lastLabels = concatLabels;
    logMessage( "Config code: " + configCode + ", Label config: " + concatLabels, true );
}
else
{
    // Matching a code means we have configured correctly
    configured = true;

    // *********************************************************** //
    // ************ NORMAL PLOTTING FUNCTIONALITY **************** //
    // *********************************************************** //
    int tempTime = json.getInt( "t" );

    JSONArray jsonGraphs = json.getJSONArray( "g" );

    for ( int i = 0; i < numGraphs; i++ )
    {
        JSONArray data = jsonGraphs.getJSONObject( i ).getJSONArray( "d" );
        double[] tempData = new double[ data.size() ];

    if (Recording) output.print(millis() + ", ");
        // Update graph objects with new data
        for ( int j = 0; j < data.size(); j++ )
        {
        tempData[j] = data.getDouble( j );
    if (Recording)  
      {
       output.print(str((float)tempData[j]));
       if (j < data.size()-1)  output.print(", ");
      } 
        }
        graphs.get( i ).Update( tempData, tempTime );
    if (Recording) output.println("");
    }

 }
}

catch ( Exception e )
{
logMessage( "Exception in serialEvent: " + e.toString(), true );
}

}

// Helper method to calculate bounds of graphs float[][] setupGraphPosition( int numGraphs ) { // Determine orientation of each graph
int numHigh = 1; int numWide = 1; // Increase num subsections in each direction until all graphs can fit while ( numHigh * numWide < numGraphs ) { if ( numWide > numHigh ) { numHigh++; } else if ( numHigh > numWide+1 ) { numWide++; } else if ( height >= width ) { numHigh++; } else { // Want to increase in high first numHigh++; }
}

float[][] posGraphs = new float[numGraphs][4];

float subHeight = round( h / numHigh );
float subWidth = round( w / numWide );

// Set bounding box for each subsection
for(int i = 0; i < numHigh; i++)
{
for (int j = 0; j < numWide; j++)
{
    int k = i * numWide + j;
    if ( k < numGraphs )
    {
    posGraphs[k][0] = i*subHeight + MARGIN_SZ / 2;
    posGraphs[k][1] = j*subWidth + MARGIN_SZ / 2;
    posGraphs[k][2] = subHeight - MARGIN_SZ;
    posGraphs[k][3] = subWidth - MARGIN_SZ;
    }
}
}

return posGraphs;

}

void attemptConnect( int index ) {
// Attempt connect on specified serial port if ( index >= Serial.list().length ) { return; } String portName = Serial.list()[portIndex]; logMessage( "Attempting connect on port: " + portName, false );

// Wrap Serial port connect in future to force timeout
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Serial> future = exec.submit( new ConnectWithTimeout( this, portName, BAUD_RATE ) );    

try
{
// Close port if another is open
if ( port != null && port.active() )
{
    port.stop();
}

// Do connect with timeout
port = future.get( CONNECT_TIMEOUT, TimeUnit.MILLISECONDS );

lastPortSwitch = millis(); // at end so that we try again immediately on invalid port
logMessage( "Connected on " + portName + ". Listening for configuration...", false );
}
catch ( TimeoutException e )
{
future.cancel( true );
logMessage( "Timed out.", true );
}
catch ( Exception e )
{
logMessage( "Exception on connect: " + e.toString(), true );    
}

exec.shutdownNow();

}

// Callable class to wrap Serial connect class ConnectWithTimeout implements Callable { private final PApplet parent; private final String portName; private final int baudRate;

public ConnectWithTimeout( PApplet parent, String portName, int baud )
{
this.parent = parent;
this.portName = portName;
this.baudRate = baud;
}

@Override
public Serial call() throws Exception
{
return new Serial( this.parent, this.portName, baudRate );
}     

}

// Logger helper void logMessage( String message, boolean debugOnly ) { if ( DEBUG || !debugOnly ) { String level = debugOnly ? "DEBUG" : "STATUS"; println( "[Time: " + millis() + " ms]" + "[" + level + "] " + message ); } }

void keyPressed() { if (key==ESC) // to exit listener { output.flush(); // Writes the remaining data to the file output.close(); // Finishes the file exit(); // Stops the program
} else if (key==' ') // to toggle record - otehr keys can be implemented to do other things Recording=!Recording; }

devinaconley commented 4 years ago

@phdv61 - would welcome a PR to add optional logging from the listener

devinaconley commented 4 years ago

Closing as this request is already being tracked here #12