Tuesday, August 30, 2016

FTC Robot Behavior Model (Part 2 - Events)

This is part 2 of a 5 part series on on a robot behavior framework.


Behaviors are connected together with events to form more complex behaviors. In this simple example, pressing the y button on the joystick will trigger a change in the active behavior from driving the robot with an an arcade style input to driving the robot with a tank style input.

The event class implements the event interface. The methods onTransition(), isTriggered(), and getToBehavior() are used by the behavior. Each method has a purpose in the framework as follows:

 - onTransition() runs only when the event is triggered resulting in a transition between behaviors. The order of transitions are set in the behaviors class. This will be covered in the part 3.
 - isTriggered() returns a boolean value. If the Boolean is True, the transition occurs.
 - getToBehavior() is used by the framework when it requests the destination behavior.


To use this behavior, copy the Event_Template shown below into a new class name. I start the name of all of my event classes with the name Event_xx. This naming convention helps when our team shares events for use in different programs. Here is a copy of the template:

package com.qualcomm.ftcrobotcontroller.geekmybot;

/** * Created by David Lempia on 8/2/2015. * * Copy this template and rename it with the name of your event. i.e. Event_ButtonX. * Inputs and Outputs are put in the Behaviors_xx file. Access them using gd.xx * Add the event to the Behaviors_xx file. * Add inputs and outputs to the Behaviors_xx file. * Pass parameters in through the Event_Template Constructor. */public class Event_Template implements Event {
    private Behavior toBehavior;

    // Returning true triggers the event    // Pass variables    public boolean isTriggered(){
        return(false);
    }

    // This runs if the event is triggered    public void onTransition(){

    }

    // Change Behaviors_XX to the name of your behaviors class    // Add parameters as needed    GlobalData gd;
    Event_Template(GlobalData gd, Behavior toBehavior){
        this.gd=gd;
        this.toBehavior = toBehavior;
    }

    // No change needed    public Behavior getToBehavior() {
        return(this.toBehavior);
    }
}

To create an event that transitions whenever the Y joystick button is pressed, add a new RiseEdgeTrigger to the constructor. Check for a button press in the isTriggered() method. Here is what the final sofware looks like for the Event_ButtonY.java class:

package com.qualcomm.ftcrobotcontroller.geekmybot;

/** * Created by David Lempia on 8/2/2015. */public class Event_ButtonY implements Event {

    // Returning true triggers the event    public boolean isTriggered(){
        boolean button_pressed = pressed.calculate(gd.buttonY);
        return(button_pressed);
    }

    // This runs if the event is triggered    public void onTransition(){

    }

    // No change needed    RiseEdgeTrigger pressed;
    private Behavior toBehavior;
    GlobalData gd;
    Event_ButtonY(GlobalData gd, Behavior toBehavior){
        this.gd = gd;
        this.toBehavior = toBehavior;
        pressed = new RiseEdgeTrigger();
    }

    // No change needed    public Behavior getToBehavior() {
        return(this.toBehavior);
    }
}

Here is the software for the RiseEdgeTrigger.java class:
 
package com.qualcomm.ftcrobotcontroller.geekmybot;

/** * Created by David Lempia on 1/17/2016. */public class RiseEdgeTrigger {
    private boolean z1=false;
    private boolean out=false;

    public boolean calculate(boolean in){

        if (in & !this.z1) this.out=true;
        else this.out=false;

        this.z1=in;

        return(this.out);
    }
}

For full copies of this software, see: https://github.com/dlacres/GeekMyBot.git 
 
If you use this behavior framework, I ask that you share the love. Print out a copy of the GeekMyBot logo and put it on your robot along with the http://geekmybot.blogspot.com/ URL address. Share this Blog with your friends.

If you develop an event that you are particularly proud of, I ask that you add a comment to this article and share your behavior along with a link to your software.

 

Monday, August 29, 2016

FTC Robot Behavior Model (Part 0)

This article is written for FTC robots using the Android Developer Studio. It describes a behavior model similar to a state machine that can be used to do very interesting things with a robot. For example, the behavior model can control a complex motion on an arm, autonomously drive the robot along a path, or interrupt a motion when a sensor detects an interfering robot. In this example I picked a very simple behavior to demonstrate how to use the behavior model. See the image below:







Pressing the Y button on the joystick will transition the drive mode of the robot from Arcade to Tank and back again.

If you use this behavior framework, I ask that you share the love. Print out a copy of the GeekMyBot logo and put it on your robot along with the http://geekmybot.blogspot.com/ URL address. Share this Blog with your friends.

If you develop a behavior that you are particularly proud of, I ask that you add a comment to this article and share your behavior along with a link to your software.

Robots are interesting when they perform a set of different behaviors. For example one behavior may be to drive using the joystick in tank mode. Another behavior may be to drive using the joystick in arcade mode.

Wouldn't it be nice if:
  • I could build a library of simple behaviors. When I need one, I just add it from the library.
  • I could build complicated behaviors by transitioning from one simple behavior to another behavior when an interesting event takes place.
  • I could re-use the behaviors in many different applications.

This article describes a simple robot behavior framework (set of Java Classes) that makes constructing behaviors and transitioning from one behavior to another easy. The article is written in 5 different parts as follows:

Part 1 - Behavior
Part 2 - Events
Part 3 - Behaviors
Part 4 - Global Data
Part 5 - OpModes


These articles are written for a two wheel drive robot that is typically used in FTC competitions. Here is a picture of the robot used in this example:






The robot has two drive motors and wheels, a left drive and a right drive. There is one omni-wheel in the back. The electronics are mounted vertically. From left to right in the picture above, the electronics are an android phone, The power distribution (the one shown is home-built using a USB hub and a 5 volt regulator), a Core Legacy Module, a Core Motor Controller, and a Core Device Interface. There is an IR seeker V3 and an integrating gyro sensor plugged into the Core Device Interface.

All software can be downloaded from - https://github.com/dlacres/GeekMyBot.git





FTC Robot Behavior Model (Part 1 - Behavior)

This is part 1 of a 5 part series on a robot behavior framework. Read Introduction to the FTC Robot Behavior Model to learn more.

The basic building block of this behavior framework is a behavior. A great behavior does one thing (behavior) and does it robustly. A behavior is active when it's onLoop() method is run by the opMode for the robot. A behavior is inactive when it is waiting for a trigger.

Events transition from one behavior to the next behavior. Events are covered in part 2 of this article.

The behavior class implements the behavior interface. The methods onEntry(), onExit(), onLoop(), addEvent(), getName(), and getEventList() are used by the framework. Each method has a purpose in the framework as follows:

 - onEntry() runs when any event results in a transition into the behavior.
 - onExit() runs when any event results in a transition out of the behavior.
 - onLoop() runs each time the robot's opmode is run.
 - addEvent() is used by the framework to add events that transition from this behavior to a list
 - getName() is used by the framework to return the name of the active behavior for display
 - getEventList() is used by the framework to return the list of events. This list is used to check if the isTriggered() method is true resulting in a transitioning to a new active behavior.

To use this behavior, copy the Behavior_Template shown below into a new class name. I start the name of all of my event classes with the name Behavior_xx where xx is a description of the behavior. This naming convention helps when our team shares behaviors for use in different programs. Here is a copy of the template:


package com.qualcomm.ftcrobotcontroller.geekmybot;

import java.util.ArrayList;
import java.util.List;

/** * Created by david lempia on 7/20/2015. * * Copy this template and rename it with the name of your behavior. i.e. Behavior_Forward. * Inputs and Outputs are put in the Behaviors_xx file. Access them using gd.xx * Add the behavior to the Behaviors_xx file. * Add inputs and outputs to the Behaviors_xx file. * Pass parameters in through the Event_Template Constructor. */public class Behavior_Template implements Behavior {
    // Change the name of this behavior (for diagnostics)    String behaviorName="none";

    // Add code for onEntry. This method is called one time when entering the behavior.    public void onEntry(){

    }
    // Add code for onExit. This method is called one time when exiting the behavior.    public void onExit(){

    }
    // Add code for onLoop. This method is called from loop as long as the behavior is active.    public void onLoop(){

    }

    // Add new classes as needed    List <Event> eventList;
    GlobalData gd;
    public Behavior_Template(GlobalData gd, String behaviorName){
        this.gd=gd;
        eventList=new ArrayList<Event>();
        this.behaviorName=behaviorName;
    }
    public List<Event> getEventList(){
        return(eventList);
    }
    public void addEvent(Event newEv){
        eventList.add(newEv);
    }

    @Override    public String getName() {
        return(this.behaviorName);
    }
}

Next, add the following code to the onLoop method:

// Drive calculations using Joystick inputs;float throttle = gd.jsLeftY/2.0f;
float direction = gd.jsLeftX/3.0f;
float right =  throttle - direction;
float left = throttle + direction;
gd.rightCmd = Range.clip(right, -1, 1);
gd.leftCmd = Range.clip(left, -1, 1);


Also, change the name of this behavior:

// Change the name of this behavior (for diagnostics)
String behaviorName="JS Arcade";



This string will be sent to the android phone connected to the joystick to indicate which behavior is active.



The JsTank behavior can likewise be created. Copy this template into a new class titled Behavior_JsTank. Add the following code to the onLoop method:

// Drive calculations using Joystick inputs;gd.rightCmd = gd.jsRightY;
gd.leftCmd = gd.jsLeftY;

 Also, change the name of this behavior:

// Change the name of this behavior (for diagnostics)
String behaviorName="JS Tank";


For full copies of this software, see: https://github.com/dlacres/GeekMyBot.git 
 
If you use this behavior framework, I ask that you share the love. Print out a copy of the GeekMyBot logo and put it on your robot along with the http://geekmybot.blogspot.com/ URL address. Share this Blog with your friends.

If you develop a behavior that you are particularly proud of, I ask that you add a comment to this article and share your behavior along with a link to your software.

 

Friday, October 24, 2014

Share Software with your Team Over Git Hub.

What happens when your main software person is not around and you want to get your robot running? What software is the latest tested version? What happens if your main computer dies right before a robotics match? Which software did I use in my last competition? What if two people edited the same file on two different computers? How do I get the changes back together again? Git is designed to help with all of these problems. Software is edited on a local computer and is pushed to the github cloud. Changes from multiple people are merged together. Any conflicts are hi-lighted and resolvable.

This article covers the basic steps in sharing FTC software files using the GIT software. This article assumes that your computer has access to the internet. It also assumes you know how to navigate the directory structure using the "cd" and "ls" commands. It also assumes that you have installed the Git Shell on your computer. 

The basic scenarios described below include create a repository in the GIT cloud, Clone the repository from the cloud to your computer, synchronize files from a computer onto the cloud, and resolve a conflict when two people change the same file.

SCENARIO 1 - Create a GIT repository in the cloud. Begin by signing up for an account on github.com. By your login name, select the pulldown menu to pick the "New Repository" menu item.



Give your repository a name and description. I will select the free public version. I also like to start my repository with a readme file. 


When you are done creating your repository, select the "Create repository" button.

SCENARIO 2 - Clone a repository from the cloud to your computer.

Begin by by getting the URL address to your github repository. On you github web page, click the copy URL button. See the copy icon on the right side of the image below.

Open your Git Shell program. In the shell window, change to the directory where you want the cloned repository. Type in the command shown below. Paste in the URL to complete the command.

git clone https://github.com/...

You now have a new git repository. Great job!

SCENARIO 3 - Pull files from your computer to the cloud. Do this before you start any edits for the day. This ensures that you have the latest files.

git pull

SCENARIO 4 - Push files from your computer to the cloud.

Open your Git Shell program. Change directory to your current working git repository. Type in the following commands:

git add FILENAME - This lets git know that something has changed. Do this whenever you change a file. You can do this multiple times before you commit. Note: git add -A adds all changes with one command.
git commit -m "Describe your change here" This gives all of the changes that you "add"ed the same comment. Do this to describe the changes you have finished. You can do this multiple times before you push.
git push - This pushes your changes to the github cloud. Check for errors. If you receive a conflict error, see SCENARIO 5 below.

SCENARIO 5 - Resolve a conflict. This will happen if someone changed the same line that you did in a file. Do this if you receive a conflict error when you try to push changes.

git pull - Download and merge the conflict into your current file. Read the comments carefully. Edit each file that was in conflict. Look for the merge marks in the file. Resolve the conflict and remove the merge marks.
git add -A
git commit -m "Resolve merge..."
git push

Merge marks look like:

<<<<<<< HEAD
Change in TestDEF
=======
Change in Test
Change in Testabc
>>>>>>> 14bf03f8eb9cef08884f85935133957d6da54db3

The merge marks are <<<, ====, and >>>>. They tell you what has changed. The <<<<<HEAD is what is in your file. After === is what is what changed in the file you pulled from github. To fix this, make a decision on what is right and fix the file. A fixed file might look like the following:

Change in TestDEF
Change in Testabc

In this case, I left both my change and the change I pulled in from github in the file.

Friday, October 3, 2014

Design Process

Design Process

The new FTC game has been announced for the year and your team is ready to build a robot. Now what? This article lists a possible set of steps that can be followed to build a great robot. It begins with generating ideas and ends with a functional robot 1 week before the first match.

1) Ideas - As a team we review the rules and brainstorm ideas to achieve game points. Use BrainWriting and BrainSwarming. The result is a chart with points at the top connected to ideas that can achieve the points. The ideas connect to robot resources needed to make the idea work. Write specific ideas down in a paper log book. See the HBR video on how do do Brainswarming to learn more.
2) Robot Story - As robot drivers, we develop a story of what what our robot does to score points during the game. Consider the autonomous, driver period, and end game. Write the story down for your team to review and to work from. After we have a basic story of what the robot does, think about what other teams will do to defend against your strategy. Present the story to the entire team. Use a top-down picture of the field and of the field elements to talk through the story. The team asks questions, offers up new ideas to add to the brainswarm chart, and considers what other teams may do to defend against the story. Write robot story and defense story down in a paper log book.
3) Design Constraints - As robot drivers, develop a list of design constraints from the story. Think about how fast, how reliably, how big/small. Review the results as a team. Write the design constraints down in a paper log book.
5) Paper Prototype Demo - The design teams take the list of ideas, the game story, and the design constraints and develops paper prototypes of the design they want. All paper prototypes are logged in the paper log book. Design is done at the module level. The frame module is detailed out first using a paper prototype. The shape of the frame must match the strategy picked by the team. Everything else will be built on this. Drive modules computing/electronics modules, and scoring module ideas fight for space on the robot frame. The paper prototype is demoed to the entire team. The group talks their way through the drive strategy and design constraints using the paper prototype as a visual. Take pictures of the paper prototypes. Put the pictures in the paper log book along with prototype descriptions.
6) Detailed Design - From paper prototypes, the team uses a CAD tool to create a detailed design for non-standard parts. CAD drawings can also be created of each module for documentation and for future re-use.
7) Working Prototype Demos - Paper prototypes and CAD drawings are turned into the working prototypes. Working prototypes result in a working demo to the entire team. The general sequence of working prototypes is a) Frame complete with discussion of where modules will hang. b) First move demo. (Add drive module, computer module, wiring, and drive software). c) First field demo. (Add one module to interact with a field element.) Demo one small part of the drive story. Repeat for each additional portion of the drive story. At the end of each demo, do a retrospective on what worked well (robot and process) and what can be improved for the next demo. Winning robots build modules that optimize the key design constraints around their story better than other robots.
8) Game Robot Demo - Select a date sometime before the first match (maybe 3 weeks). Each design module team and the SW team decides what is reasonable to have done 1 week before the first match. Prioritize the work that will be done to make sure the most important parts of the robot story work. All CAD design parts need to be complete and attached to their modules. Parts that are not very reliable should have duplicates made for the game match.

Monday, September 15, 2014

FTC Software Toolbox - Rise Edge / Fall Edge

What is your favorite software tool? This post describes some of the tried and true software tools that I have found useful. I would love to hear from you about tools you find useful. Please put a comment at the bottom of this post with a description or a pointer to tools you find useful.

Today we will look at a utility that is very useful to filter button pushes and state machine events. This utility finds the rising edge or a falling edge on a signal.

// Story: As a software designer I want to find the rising edge of a signal so that I can trigger an event
// Usage: Make sure globalBoolEqF is initialized equal to false
//   bool globalBoolEqF = false; //global
//   outBool = RiseEdge( inBool, globalBoolEqF );
// Author: David Lempia at GeekMyBot.Blogspot.com
bool RiseEdge(bool in, bool &z1)
{
  bool out;

  out=false;
  if ((in==true) && (z1==false)) out=true;
  z1=in;

  return(out);
}

#define TEST_RISE_EDGE
#ifdef TEST_RISE_EDGE //-------------------------------------------------
// Add this code to a while loop in main
// Unit Test
// [x] Rise Edge of input results in a true output for 1 frame
// [x] Fall Edge of input results in no change to the output
// [x] True input for more than one frame results in a false output
// [x] False input results in a false output
#include "i_debug.c"

task main(){
  bool z=false;
  bool out,in=false;

  for (int i=0; i<10; i++){

    if (i<2) in=false;
    else if (i<4) in=true;
    else if (i<6) in=false;
    else if (i<8) in=true;

    out=RiseEdge(in, z); //Code to test

    DebugBool("In",in);
    DebugBool("Out",out);
    DebugPrint();
  }
}
#endif


Here is the watch window (tabular format) for the tests I ran on a RiseEdge function:



Here is the plot version for the tests I ran on a RiseEdge function:



Whenever the input signal transitions from a false to a true, the output goes true for one frame. This is a great way to trigger an event.
Likewise, here is the software for a falling edge:

// Story: As a software designer I want to find the falling edge of a signal so that I can trigger an event
// Usage: Make sure globalBoolEq0 is initialized equal to false
// outBool = RiseEdge( inBool, globalBoolEqF );
// Author: David Lempia at GeekMyBot.Blogspot.com
bool FallEdge(bool in, bool &z1)
{
 bool out;

 out=false;
  if ((in==false) && (z1==true)) out=true;
  z1=in;

 return(out);
}
#define TEST_FALL_EDGE
#ifdef TEST_FALL_EDGE //-------------------------------------------------
// Add this code to a while loop in main
// Unit Test
// [x] Fall Edge of input results in a true output for 1 frame
// [x] Rise Edge of input results in no change to the output
// [x] True input for more than one frame results in a false output
// [x] False input results in a false output
#include "i_debug.c"

task main(){
  bool z=false;
  bool out,in=false;

  for (int i=0; i<10; i++){

    if (i<2) in=false;
    else if (i<4) in=true;
    else if (i<6) in=false;
    else if (i<8) in=true;

    out=FallEdge(in, z); //Code to test

    DebugBool("In",in);
    DebugBool("Out",out);
    DebugPrint();
  }
}
#endif


Here is the resulting output
Let me know if you find this function useful.

Thursday, September 4, 2014

FTC Software Toolbox - Debug

What is your favorite software tool? This post describes some of the tried and true software tools that I have found useful. I would love to hear from you about tools you find useful. Please put a comment at the bottom of this post with a description or a pointer to tools you find useful.

Today we will look at my favorite module, Debug. Of course, setting breakpoints and debugging in Robot C is amazing. Sometimes, however, it is helpful to look at how the program is running using something less intrusive than Robot C debug and more friendly than a print statement. This debug program runs in the background. Results can be viewed in tabular format and as a time-history plot when the robot is reconnected to the PC and Robot C.

#pragma DebuggerWindows("debugstream")
// Story - As a software debugger, I want to see a descriptive string and a variable so that I can understand
//    what my software is doing.
// Usage - To use this software,
//   1. DebugSkip(4); Print out every 5th value. Use any number you want. The default is 0 (no skips).
//   2. Use DebugInt, DebugFloat, or DebugBool to collect the debug information.
//   3. Place the DebugPrint(); after all the debugInt calls. (Takes 10ms or less to run).
//
//   The data is output to the debug stream and to a file called "debug.txt"
//   The data going to the file is limited to 10000 characters. This is about 151 rows with 10 columns.
//
//  Written by David Lempia
//
#define DBG_MAX_COL 10
string debugColName[10];
string debugValue[10];
int debugI=0;
int debugSkip=0;
int debugSkipI=0;
int iFrameCntDebug=0;

short DebugFile;
TFileIOResult isOk;
int nFileSize=10000;
string debugFileName="debug.txt";

// ---------------- Capture integer debug information ---------------------//
void DebugInt(const string name, int value){
  string tmp;
  if (debugI < DBG_MAX_COL){
    if (iFrameCntDebug==0){
      strncat(tmp,name,5);
      sprintf(debugColName[debugI],"%5s",tmp);
    }
    sprintf(debugValue[debugI++],"%5d",value);
  }
  return;
}
// ---------------- Capture float debug information ---------------------//
void DebugFloat(const string name, float value){
  string tmp;
  if (debugI < DBG_MAX_COL){
    if (iFrameCntDebug==0){
      strncat(tmp,name,5);
      sprintf(debugColName[debugI],"%5s",tmp);
    }
    sprintf(debugValue[debugI++],"% 2.2f",value);
  }
  return;
}
// ---------------- Capture boolean debug information ---------------------//
void DebugBool(const string name, bool value){
  string tmp;
  if (debugI < DBG_MAX_COL){
    if (iFrameCntDebug==0){
      strncat(tmp,name,5);
      sprintf(debugColName[debugI],"%5s",tmp);
    }
    if (value==true)
      debugValue[debugI++]="    T";
    else
      debugValue[debugI++]="    F";
  }
  return;
}

// Set the number of times the printout is skipped (Default is to print every time)
void DebugSkip(int skip){
  debugSkip=skip;
}

// Send the print data to the debug window and the debug file "debug.txt"
void DebugPrint(){
  char str[70];
  string tmp1,tmp2;
  int i=0;

  if (debugI>0){// Do not send out Debug info if not needed
    if (iFrameCntDebug==0){
      //writeDebugStream("col size %d", debugI);
      Delete(debugFileName, isOk);
      OpenWrite(DebugFile, isOk, debugFileName, nFileSize);

      strcpy(str,"Frame,");
      for (i=0; i < (debugI-1); ++i){
        strcat(str,debugColName[i]);
        strcat(str,",");
      }
      strcat(str,debugColName[i]);
      strcat(str, "\n");

      writeDebugStream(str);
      WriteString(DebugFile, isOk, str);
    }

    if (debugSkip < debugSkipI++){
      strcpy(str,"");
      sprintf(str,"%5d,",iFrameCntDebug);
      for (i=0; i < (debugI-1); ++i){
        strcat(str,debugValue[i]);
        strcat(str,",");
      }
      strcat(str,debugValue[i]);
      strcat(str,"\n");

      writeDebugStream(str);
      WriteString(DebugFile, isOk, str);

      debugSkipI=0;
    }
    iFrameCntDebug++;
    debugI=0; //Clear the debug buffer
  }
}
//#define TEST_DEBUG //-------------------------------------------------
#ifdef TEST_DEBUG
// Unit Test
// [x] Normal test
// [x] To many debugInt(3) calls
// [x] No calls to debugInt() - No output should happen.
// [x] Skip number is right
// [x] The name is longer than 5 characters. Should be limited to 5.
// [x] Output to file and debug stream match
// [x] Test max file size (File output ends without issue when the max file is hit)
// [x] Test worst case time. It takes between 3 and 10 ms to output all 10 columns of data.
task main(){
  int tmp=0;
  //DebugSkip(2); //

  ClearTimer(T2);

  for (int i=0; i < 10; i++){
    DebugInt("r1",time1[T2]);
    DebugInt("r2",time1[T2]);
    DebugInt("r3",time1[T2]);
    DebugInt("r4",time1[T2]);
    DebugInt("r5",time1[T2]);
    DebugInt("r6",time1[T2]);
    DebugInt("r7",time1[T2]);
    DebugInt("r8",time1[T2]);
    DebugInt("r9",time1[T2]);
    DebugInt("r10",time1[T2]);
    DebugInt("r11",time1[T2]);

    /*DebugInt("1234567",tmp++);
    DebugFloat("r2",2.5);
    DebugBool("r3",false);*/

    //-----------------Print the debug statements out------------------//
    DebugPrint();
  }
}
#endif

Correct usage of the debug software requires a minimum of two function calls, a DebugInt(), DebugFloat(), or DebugBool() (i.e. watch functions) to capture the watch information, and a DebugPrint() to send the print information out to the debug window and the debug file. Up to 10 watch variables can be watched at a time.

Use DebugInt, DebugFloat, or DebugBool function calls to watch variables in the software. Be careful to place the function calls in a place where they will be called on every itteration. If the calls are inside an if-then-else and are not called on an itteration, the past value will be printed out.

Place DebugPrint() in a location that is called after all of the watch functions are called. That is all there is to it. Here is the watch window (tabular format) for the tests I ran on a RiseEdge function:



Here is the plot version for the tests I ran on a RiseEdge function:


To plot this file, I copied the file off of the NXT using Robot C the main menu-> Robot -> NXT Brick -> File Management Utility. I then opened the file in MS Excel and used a line plot.

Let me know if you find this function useful.