Boat Data Logger

Boat Data Logger

This is a module that will log data like pitch, roll, gps coords on your boat and store the data to a sdcard for analysis.

  • 195 views
  • 0 comments
  • 0 respects

Components and supplies

Ardgen 101
Arduino 101 & Genuino 101
×1
gps module
×1
sd card reader
×1
sd card
×1

About this project

I wanted to create a device that would log the movement of a boat while fishing. The features of the Arduino 101 contained much of the hardware required to do this. The only pieces missing was a GPS sensor and a SD card reader/writer and a battery.

The Arduino 101 is ideal because it contained a 6 axis accelerometer and gyroscope.

In my project, I used the accelerometer and gyroscope data from the Arduino 101, multiple pieces of data from the GPS and write that data to a SD card. I was also able to use Aritro Mukherjee - DIY Flight Instruments for Horizon and Compass with a few modifications to connect the data logger to a PC using over the usb debug port that allowed me to add visualization to what to logger was doing.

A few things that would like to add to this project would be to use the onboard bluetooth to send the data to a mobile device, either a phone or tablet, and to convert the PC part to a mobile app. So that the everything could be mounted and wireless.

Some code changes I plan to make are:

  • Add a serial event, this will allow me to make configuration changes to the data logger using the processing app.
  • Add a map to the processing app, this will allow me to plot the gps coordinates from the data logger.
  • Way points, it would be nice to send the data logger a way point and have the processing app point the direction to it.

Some hardware changes:

  • A compass module, it would be nice to have a true compass instead of using the gps and accelerometer/gyroscope to determine direction.
  • Temperature sensor

All in all, this project was fairly simple to make, most of the code was taken from samples for the various parts and altered a bit to fit my needs. Hopefully if you are looking at this project your able to use the code provided to help build your own project. In the code I had left a lot of print statements so you can use the serial monitor to follow what the code is doing. Otherwise if you need to save space most of the print statements can be removed.

Thanks.

Code

Arduino codeArduino
//required for sdcard
#include <SPI.h>
#include <SD.h>
//required for gps
#include <TinyGPS++.h>
#include <SoftwareSerial.h>
//required for IMU
#include <CurieIMU.h>
#include <MadgwickAHRS.h>


const bool DEBUG = false;

static const int RXPin = 7, TXPin = 8;  //Pins for gps module serial port
static const uint32_t GPSBaud = 9600;   // gps baud rate, the module I have use 9600
SoftwareSerial ss(RXPin, TXPin);        // The serial connection to the GPS device
TinyGPSPlus gps;                        // The TinyGPS++ object

unsigned long last = 0UL;               // For stats that happen every 5 seconds

const int chipSelect = 10;              //sd card

//IMU
Madgwick filter;
unsigned long microsPerReading, microsPrevious;
float accelScale, gyroScale;

void setup()
{
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  Serial.print("Initializing SD card...");

  // see if the card is present and can be initialized:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    return;
  }
  Serial.println("card initialized.");
  
  ss.begin(GPSBaud);

  Serial.println(F("imu1gps5ssd1.ino"));

  Serial.println(TinyGPSPlus::libraryVersion());

  Serial.println();

  //IMU
    // start the IMU and filter
  CurieIMU.begin();
  CurieIMU.setGyroRate(25);
  CurieIMU.setAccelerometerRate(25);
  filter.begin(25);

  // Set the accelerometer range to 2G
  CurieIMU.setAccelerometerRange(2);
  // Set the gyroscope range to 250 degrees/second
  CurieIMU.setGyroRange(250);

   //Serial.print("Starting Gyroscope calibration...");
    CurieIMU.autoCalibrateGyroOffset();
    //Serial.println(" Done");
    //Serial.print("Starting Acceleration calibration...");
    CurieIMU.autoCalibrateXAccelOffset(0);
    CurieIMU.autoCalibrateYAccelOffset(0);
    CurieIMU.autoCalibrateZAccelOffset(1);
    //Serial.println(" Done");

    //Serial.println("Enabling Gyroscope/Acceleration offset compensation");
    CurieIMU.setGyroOffsetEnabled(true);
    CurieIMU.setAccelOffsetEnabled(true);
  // initialize variables to pace updates to correct rate
  microsPerReading = 1000000 / 25;
  microsPrevious = micros();
}

void loop(){

   // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  File dataFile = SD.open("gps.txt", FILE_WRITE);

  // if the file is available, write to it:
  if (dataFile) { 
    // Dispatch incoming characters
    while (ss.available() > 0){
      char c = ss.read();
      dataFile.write(c);
      gps.encode(c);
    }
    String imuRetVal = imu();
    dataFile.println(imuRetVal);
    //Serial.println("IMU Return Value:" + imuRetVal);
    dataFile.close();
  } else {
    Serial.println("error opening datalog.txt");// if the file isn't open, pop up an error:
  }
  
   if (gps.location.isUpdated()){
      Serial.print(F("LOCATION   Fix Age="));
      Serial.print(gps.location.age());
      Serial.print(F("ms Raw Lat="));
      Serial.print(gps.location.rawLat().negative ? "-" : "+");
      Serial.print(gps.location.rawLat().deg);
      Serial.print("[+");
      Serial.print(gps.location.rawLat().billionths);
      Serial.print(F(" billionths],  Raw Long="));
      Serial.print(gps.location.rawLng().negative ? "-" : "+");
      Serial.print(gps.location.rawLng().deg);
      Serial.print("[+");
      Serial.print(gps.location.rawLng().billionths);
      Serial.print(F(" billionths],  Lat="));
      Serial.print(gps.location.lat(), 6);
      Serial.print(F(" Long="));
      Serial.println(gps.location.lng(), 6);
    } else if (gps.date.isUpdated()){
      Serial.print(F("DATE       Fix Age="));
      Serial.print(gps.date.age());
      Serial.print(F("ms Raw="));
      Serial.print(gps.date.value());
      Serial.print(F(" Year="));
      Serial.print(gps.date.year());
      Serial.print(F(" Month="));
      Serial.print(gps.date.month());
      Serial.print(F(" Day="));
      Serial.println(gps.date.day());
    } else if (gps.time.isUpdated()){
      Serial.print(F("TIME       Fix Age="));
      Serial.print(gps.time.age());
      Serial.print(F("ms Raw="));
      Serial.print(gps.time.value());
      Serial.print(F(" Hour="));
      Serial.print(gps.time.hour());
      Serial.print(F(" Minute="));
      Serial.print(gps.time.minute());
      Serial.print(F(" Second="));
      Serial.print(gps.time.second());
      Serial.print(F(" Hundredths="));
      Serial.println(gps.time.centisecond());
    } else if (gps.speed.isUpdated()){
      Serial.print(F("SPEED      Fix Age="));
      Serial.print(gps.speed.age());
      Serial.print(F("ms Raw="));
      Serial.print(gps.speed.value());
      Serial.print(F(" Knots="));
      Serial.print(gps.speed.knots());
      Serial.print(F(" MPH="));
      Serial.print(gps.speed.mph());
      Serial.print(F(" m/s="));
      Serial.print(gps.speed.mps());
      Serial.print(F(" km/h="));
      Serial.println(gps.speed.kmph());
    } else if (gps.course.isUpdated()){
      Serial.print(F("COURSE     Fix Age="));
      Serial.print(gps.course.age());
      Serial.print(F("ms Raw="));
      Serial.print(gps.course.value());
      Serial.print(F(" Deg="));
      Serial.println(gps.course.deg());
    } else if (gps.altitude.isUpdated()){
      Serial.print(F("ALTITUDE   Fix Age="));
      Serial.print(gps.altitude.age());
      Serial.print(F("ms Raw="));
      Serial.print(gps.altitude.value());
      Serial.print(F(" Meters="));
      Serial.print(gps.altitude.meters());
      Serial.print(F(" Miles="));
      Serial.print(gps.altitude.miles());
      Serial.print(F(" KM="));
      Serial.print(gps.altitude.kilometers());
      Serial.print(F(" Feet="));
      Serial.println(gps.altitude.feet());
    } else if (gps.satellites.isUpdated()){
      Serial.print(F("SATELLITES Fix Age="));
      Serial.print(gps.satellites.age());
      Serial.print(F("ms Value="));
      Serial.println(gps.satellites.value());
    } else if (gps.hdop.isUpdated()){
      Serial.print(F("HDOP       Fix Age="));
      Serial.print(gps.hdop.age());
      Serial.print(F("ms Value="));
      Serial.println(gps.hdop.value());
    } else if (millis() - last > 5000){
      Serial.println();
      if (gps.location.isValid()){
//        static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002;
//        double distanceToLondon =
//          TinyGPSPlus::distanceBetween(
//            gps.location.lat(),
//            gps.location.lng(),
//            LONDON_LAT, 
//            LONDON_LON);
//        double courseToLondon =
//          TinyGPSPlus::courseTo(
//            gps.location.lat(),
//            gps.location.lng(),
//            LONDON_LAT, 
//            LONDON_LON);
  
//        Serial.print(F("LONDON     Distance="));
//        Serial.print(distanceToLondon/1000, 6);
//        Serial.print(F(" km Course-to="));
//        Serial.print(courseToLondon, 6);
//        Serial.print(F(" degrees ["));
//        Serial.print(TinyGPSPlus::cardinal(courseToLondon));
//        Serial.println(F("]"));
      }
  
      Serial.print(F("DIAGS      Chars="));
      Serial.print(gps.charsProcessed());
      Serial.print(F(" Sentences-with-Fix="));
      Serial.print(gps.sentencesWithFix());
      Serial.print(F(" Failed-checksum="));
      Serial.print(gps.failedChecksum());
      Serial.print(F(" Passed-checksum="));
      Serial.println(gps.passedChecksum());

      //Send the gps coords out
      Serial.print("$GPS,");
      Serial.print(gps.location.lat());
      Serial.print( "," );
      Serial.print(gps.location.lng());
      Serial.println(" ");

      //send the data out
      Serial.print("$DATE,");
      Serial.print(gps.date.year());
      Serial.print(F("/"));
      Serial.print(gps.date.month());
      Serial.print(F("/"));
      Serial.println(gps.date.day());

      //Send the time out.
      Serial.print("$TIME,");
      Serial.print(gps.time.hour());
      Serial.print(":");
      Serial.print(gps.time.minute());
      Serial.print(":"); 
      Serial.print(gps.time.second());
      Serial.print(" GMT");
      
      if (gps.charsProcessed() < 10)
        Serial.println(F("WARNING: No GPS data.  Check wiring."));
  
      last = millis();
      Serial.println();
   } 
}

String imu(){
  int aix, aiy, aiz;
  int gix, giy, giz;
  float ax, ay, az;
  float gx, gy, gz;
  float roll, pitch, heading;
  unsigned long microsNow;
  
  String retVal = "$IMU,";
   
  // check if it's time to read data and update the filter
  microsNow = micros();
  if (microsNow - microsPrevious >= microsPerReading) {

    // read raw data from CurieIMU
    CurieIMU.readMotionSensor(aix, aiy, aiz, gix, giy, giz);

    // convert from raw data to gravity and degrees/second units
    ax = convertRawAcceleration(aix);
    ay = convertRawAcceleration(aiy);
    az = convertRawAcceleration(aiz);
    gx = convertRawGyro(gix);
    gy = convertRawGyro(giy);
    gz = convertRawGyro(giz);

    // update the filter, which computes orientation
    filter.updateIMU(gx, gy, gz, ax, ay, az);

    // print the heading, pitch and roll
    roll = filter.getRoll();
    pitch = filter.getPitch();
    heading = filter.getYaw();

    retVal = retVal + roll + "," + pitch + "," + heading;

    Serial.println(retVal);
    
    if (DEBUG){
      Serial.print("Orientation: ");
      Serial.print(heading);
      Serial.print(" ");
      Serial.print(pitch);
      Serial.print(" ");
      Serial.println(roll);
    }
    // increment previous time, so we keep proper pace
    microsPrevious = microsPrevious + microsPerReading;
  }
  return retVal;
}


float convertRawAcceleration(int aRaw) {
  // since we are using 2G range
  // -2g maps to a raw value of -32768
  // +2g maps to a raw value of 32767
  
  float a = (aRaw * 2.0) / 32768.0;
  return a;
}

float convertRawGyro(int gRaw) {
  // since we are using 250 degrees/seconds range
  // -250 maps to a raw value of -32768
  // +250 maps to a raw value of 32767
  
  float g = (gRaw * 250.0) / 32768.0;
  return g;
}
Processing codeJava
import org.firmata.*;
import cc.arduino.*;

//Thanks to Adrian Fernandez for the beta-version
//Modified and updated as per latest IDE by Aritro Mukherjee (April,2016)
//Check the detailed tutorial @ www.hackster.io/Aritro


import processing.serial.*;
import cc.arduino.*;

int W=1400; //My Laptop's screen width 
int H=700;  //My Laptop's screen height 
float Pitch; 
float Bank; 
float Azimuth; 
float ArtificialHoizonMagnificationFactor=0.7; 
float CompassMagnificationFactor=0.85; 
float SpanAngle=120; 
int NumberOfScaleMajorDivisions; 
int NumberOfScaleMinorDivisions; 
PVector v1, v2; 


Serial port;
float Phi;    //Dimensional axis
float Theta;
float Psi;

String Lat = "";
String Lng = "";
String gDate = "";
String gTime = "";

void setup() 
{ 
  size(1400, 700); 
  rectMode(CENTER); 
  smooth(); 
  strokeCap(SQUARE);//Optional 
  
  //println(Serial.list()); //Shows your connected serial ports
  //port = new Serial(this, Serial.list()[0], 115200); 
  port = new Serial(this, Serial.list()[1], 9600);
  println(Serial.list()[1]);
  //Up there you should select port which arduino connected and same baud rate.
  port.bufferUntil('\n'); 
}
void draw() 
{ 
  background(0); 
  translate(W/4, H/2.1);  
  MakeAnglesDependentOnMPU6050(); 
  Horizon(); 
  rotate(-Bank); 
  PitchScale(); 
  Axis(); 
  rotate(Bank); 
  Borders(); 
  Plane(); 
  ShowAngles(); 
  Compass(); 
  ShowAzimuth(); 
  delay(500);
}
void serialEvent(Serial port) //Reading the datas by Processing.
{
   String input = port.readStringUntil('\n');
  if(input != null){
    input = trim(input);
    //String[] values = split(input, '\t');
    String[] values = split(input, ',');
    //println(input);
    if (values[0].equals("$IMU")){
      if(values.length == 4){
        float phi = float(values[1]);
        float theta = float(values[2]); 
        float psi = float(values[3]); 
        psi = psi -.05;
        print(phi);
        print(" ");
        print(theta);
        print(" ");
        println(psi);
        Phi = phi;
        Theta = theta;
        Psi = psi;
      }
    } 
    if (values[0].equals("$GPS")){
      if(values.length == 3){
        String lat = values[1];
        String lng = values[2];
        Lat = lat;
        Lng = lng;
      }
    }  
    if (values[0].equals("$DATE")){
      if(values.length == 2){
        String date = values[1];
        gDate = date;
        println(gDate);
      }
    } 
    if (values[0].equals("$TIME")){
      if(values.length == 2){
        String time = values[1];
        gTime = time;
        println(gTime);
      }
    } 
  }
}
void MakeAnglesDependentOnMPU6050() 
{ 
  //Bank =-Phi/5; 
  //Pitch=Theta*10; 
  //Azimuth=Psi;
  
  //Bank =Phi/1000;
  //Pitch=Theta/1000; 
  //Azimuth=Psi;
  
  Bank=-Phi/100;
  Bank= float(nf(Bank, 3, 2));
  //Bank=0;
  //println(Theta);
  if (Theta > 400) Theta = 400;
  if (Theta < -400) Theta = -400;
  Pitch=Theta * 5; 
  Azimuth=-Psi;
}
void Horizon() 
{ 
  scale(ArtificialHoizonMagnificationFactor); 
  noStroke(); 
  fill(0, 180, 255); 
  rect(0, -100, 900, 1000); 
  fill(95, 55, 40); 
  rotate(-Bank); 
  rect(0, 400+Pitch, 900, 800); 
  rotate(Bank); 
  rotate(-PI-PI/6); 
  SpanAngle=120; 
  NumberOfScaleMajorDivisions=12; 
  NumberOfScaleMinorDivisions=24;  
  CircularScale(); 
  rotate(PI+PI/6); 
  rotate(-PI/6);  
  CircularScale(); 
  rotate(PI/6); 
}
void ShowAzimuth() 
{ 
  fill(50); 
  noStroke(); 
  rect(20, 470, 440, 50); 
  int Azimuth1=round(Azimuth); 
  textAlign(CORNER); 
  textSize(35); 
  fill(255); 
  text("Azimuth:  "+Azimuth1+" Deg", 80, 477, 500, 60); 
  textSize(40);
  fill(25,25,150);
  text("SIMULATOR", -350, 477, 500, 60);
  fill(255);
  //text("Lat:" + Lat, -450, 520, 500, 60); 
  //text("Lon:" + Lng, -450, 565, 500, 60); 
  //text("Date:" + gDate, -150, 520, 500, 60); 
  //text("Time:" + gTime, -150, 565, 500, 60); 
}
void Compass() 
{ 
  translate(2*W/3, 0); 
  scale(CompassMagnificationFactor); 
  noFill(); 
  stroke(100); 
  strokeWeight(80); 
  ellipse(0, 0, 750, 750); 
  strokeWeight(50); 
  stroke(50); 
  fill(0, 0, 40); 
  ellipse(0, 0, 610, 610); 
  for (int k=255;k>0;k=k-5) 
  { 
    noStroke(); 
    fill(0, 0, 255-k); 
    ellipse(0, 0, 2*k, 2*k); 
  } 
  strokeWeight(20); 
  NumberOfScaleMajorDivisions=18; 
  NumberOfScaleMinorDivisions=36;  
  SpanAngle=180; 
  CircularScale(); 
  rotate(PI); 
  SpanAngle=180; 
  CircularScale(); 
  rotate(-PI); 
  fill(255); 
  textSize(60); 
  textAlign(CENTER); 
  text("W", -375, 0, 100, 80); 
  text("E", 370, 0, 100, 80); 
  text("N", 0, -365, 100, 80); 
  text("S", 0, 375, 100, 80); 
  textSize(30); 
  text("COMPASS", 0, -130, 500, 80); 
  rotate(PI/4); 
  textSize(40); 
  text("NW", -370, 0, 100, 50); 
  text("SE", 365, 0, 100, 50); 
  text("NE", 0, -355, 100, 50); 
  text("SW", 0, 365, 100, 50); 
  rotate(-PI/4); 
  CompassPointer(); 
}
void CompassPointer() 
{ 
  rotate(PI+radians(Azimuth));  
  stroke(0); 
  strokeWeight(4); 
  fill(100, 255, 100); 
  triangle(-20, -210, 20, -210, 0, 270); 
  triangle(-15, 210, 15, 210, 0, 270); 
  ellipse(0, 0, 45, 45);   
  fill(0, 0, 50); 
  noStroke(); 
  ellipse(0, 0, 10, 10); 
  triangle(-20, -213, 20, -213, 0, -190); 
  triangle(-15, -215, 15, -215, 0, -200); 
  rotate(-PI-radians(Azimuth)); 
}
void Plane() 
{ 
  fill(0); 
  strokeWeight(1); 
  stroke(0, 255, 0); 
  triangle(-20, 0, 20, 0, 0, 25); 
  rect(110, 0, 140, 20); 
  rect(-110, 0, 140, 20); 
}
void CircularScale() 
{ 
  float GaugeWidth=800;  
  textSize(GaugeWidth/30); 
  float StrokeWidth=1; 
  float an; 
  float DivxPhasorCloser; 
  float DivxPhasorDistal; 
  float DivyPhasorCloser; 
  float DivyPhasorDistal; 
  strokeWeight(2*StrokeWidth); 
  stroke(255);
  float DivCloserPhasorLenght=GaugeWidth/2-GaugeWidth/9-StrokeWidth; 
  float DivDistalPhasorLenght=GaugeWidth/2-GaugeWidth/7.5-StrokeWidth;
  for (int Division=0;Division<NumberOfScaleMinorDivisions+1;Division++) 
  { 
    an=SpanAngle/2+Division*SpanAngle/NumberOfScaleMinorDivisions;  
    DivxPhasorCloser=DivCloserPhasorLenght*cos(radians(an)); 
    DivxPhasorDistal=DivDistalPhasorLenght*cos(radians(an)); 
    DivyPhasorCloser=DivCloserPhasorLenght*sin(radians(an)); 
    DivyPhasorDistal=DivDistalPhasorLenght*sin(radians(an));   
    line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal); 
  }
  DivCloserPhasorLenght=GaugeWidth/2-GaugeWidth/10-StrokeWidth; 
  DivDistalPhasorLenght=GaugeWidth/2-GaugeWidth/7.4-StrokeWidth;
  for (int Division=0;Division<NumberOfScaleMajorDivisions+1;Division++) 
  { 
    an=SpanAngle/2+Division*SpanAngle/NumberOfScaleMajorDivisions;  
    DivxPhasorCloser=DivCloserPhasorLenght*cos(radians(an)); 
    DivxPhasorDistal=DivDistalPhasorLenght*cos(radians(an)); 
    DivyPhasorCloser=DivCloserPhasorLenght*sin(radians(an)); 
    DivyPhasorDistal=DivDistalPhasorLenght*sin(radians(an)); 
    if (Division==NumberOfScaleMajorDivisions/2|Division==0|Division==NumberOfScaleMajorDivisions) 
    { 
      strokeWeight(15); 
      stroke(0); 
      line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal); 
      strokeWeight(8); 
      stroke(100, 255, 100); 
      line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal); 
    } 
    else 
    { 
      strokeWeight(3); 
      stroke(255); 
      line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal); 
    } 
  } 
}
void Axis() 
{ 
  stroke(255, 0, 0); 
  strokeWeight(3); 
  line(-115, 0, 115, 0); 
  line(0, 280, 0, -280); 
  fill(100, 255, 100); 
  stroke(0); 
  triangle(0, -285, -10, -255, 10, -255); 
  triangle(0, 285, -10, 255, 10, 255); 
}
void ShowAngles() 
{ 
  textSize(30); 
  fill(50); 
  noStroke(); 
  rect(-150, 400, 280, 40); 
  rect(150, 400, 280, 40); 
  rect(-150, 449, 280, 40); 
  rect(-150, 496, 280, 40); 
  rect(150, 449, 280, 40);
  rect(150, 496, 280, 40); 
  fill(255); 
  Pitch=Pitch/5; 
  int Pitch1=round(Pitch);  
  text("Pitch:  "+Pitch1+" Deg", -20, 411, 500, 60); 
  text("Bank:  "+Bank*100+" Deg", 280, 411, 500, 60); 
  text("Lat: " + Lat, -20, 460, 500, 60); 
  text("" + gDate, -20, 505, 500, 60);
  text("Lng: " + Lng, 280, 460, 500, 60); 
  text("" + gTime, 280, 505, 500, 60); 
}
void Borders() 
{ 
  noFill(); 
  stroke(0); 
  strokeWeight(400); 
  rect(0, 0, 1100, 1100); 
  strokeWeight(200); 
  ellipse(0, 0, 1000, 1000); 
  fill(0); 
  noStroke(); 
  rect(4*W/5, 0, W, 2*H); 
  rect(-4*W/5, 0, W, 2*H); 
}
void PitchScale() 
{  
  stroke(255); 
  fill(255); 
  strokeWeight(3); 
  textSize(24); 
  textAlign(CENTER); 
  for (int i=-4;i<5;i++) 
  {  
    if ((i==0)==false) 
    { 
      line(110, 50*i, -110, 50*i); 
    }  
    text(""+i*10, 140, 50*i, 100, 30); 
    text(""+i*10, -140, 50*i, 100, 30); 
  } 
  textAlign(CORNER); 
  strokeWeight(2); 
  for (int i=-9;i<10;i++) 
  {  
    if ((i==0)==false) 
    {    
      line(25, 25*i, -25, 25*i); 
    } 
  } 
}

Schematics

Wiring Diagram
Data logger  g9zlm8v8we

Comments

Add projectSign up / Login