Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Attila Tőkés
Published © CC BY

Spresense GPS Enabled Action / Dash Cam

Action cam built using Sony Spresense featuring video (JPEG), audio (MP3) and GPS recording.

IntermediateFull instructions provided24 hours2,630

Things used in this project

Hardware components

Spresense boards (main & extension)
Sony Spresense boards (main & extension)
×1
Sony Spresense camera board
×1
SparkFun Electret Microphone Breakout
SparkFun Electret Microphone Breakout
electret microphone
×2
Speaker: 0.25W, 8 ohms
Speaker: 0.25W, 8 ohms
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Enclosure (v2)

CAD files - STEP + Autodesk Inventor

Spresense Main Board + Camera - basic model
Enclosure

Schematics

Schematics

Code

Action Cam Main

Arduino
/*
 *  SpresenseActionCam.ino
 *  Copyright 2018 Sony Semiconductor Solutions Corporation
 *  Copyright 2018 Attila Tőkés
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include <SDHCI.h>
#include <stdio.h>

#include "AviMJPEG.hpp"
#include "Gnns.hpp"

#include <Audio.h>
#include <Camera.h>

SDClass  mySD;

/** Setup & Loop functions of each component **/
void setupCamera();
void loopCamera();

void setupAudio();
void loopAudio();

void setupGps();
void loopGps();

void setupUsb();
void loopUsb();

/**
 * @brief Call setup of each subcomponent.
 */
void setup()
{
  Serial.begin(115200);
  while (!Serial) ; /* wait for the serial port */

  Serial.println("Seting up the camera.");
  setupCamera();
  
  Serial.println("Seting up the Audio system.");
  setupAudio();
  
  Serial.println("Seting up the GPS system.");
  setupGps();

  Serial.println("Seting up the USB mass storage.");
  setupUsb();
}

/**
 * @brief Take picture with format JPEG per second
 */

void loop()
{  
  loopCamera();
  loopAudio();
  loopGps();
  loopUsb();
}


/******* CAMERA STUFF *******/

AviMJPEG *aviMjpeg;
int video_frames_count = 0;
int take_picture_count = 0;
File outFile;

/**
 * Callback from Camera library when video frame is captured.
 */
void CamCB(CamImage img) {
  /* Check the img instance is available or not. */
  if (img.isAvailable()) {      
      // note: real-time streaming not working with JPEG
      //video_frames_count++;
      //aviMjpeg->writeFrame(img.getImgBuff(), img.getImgSize());

      //if (video_frames_count >= someNumberOfFrames) {         
        // aviMjpeg->writeEnd();
        // video_frames_count = 0;
        // start new video file
      //}
  } else {
    Serial.print("Failed to get video stream image\n");
  }
}

void setupCamera() {
  /* begin() without parameters means that
   * number of buffers = 1, 30FPS, QVGA, YUV 4:2:2 format */

  Serial.println("Prepare camera");
  CamErr camErr = theCamera.begin(
      1, /* buff_num */
      CAM_VIDEO_FPS_30, /* fps */
      CAM_IMGSIZE_QVGA_H, /* video_width */
      CAM_IMGSIZE_QVGA_V, /* video_height */
      CAM_IMAGE_PIX_FMT_YUV422 /* video_height */
  );
  Serial.println(camErr);

  /* Start video stream.
   * If received video stream data from camera device,
   *  camera library call CamCB.
   */

  //Serial.println("Start streaming");
  //theCamera.startStreaming(true, CamCB);

  /* Auto white balance configuration */

  Serial.println("Set Auto white balance parameter");
  theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT);
 
  Serial.println("Start streaming");

  theCamera.setStillPictureImageFormat(
     CAM_IMGSIZE_QUADVGA_H,
     CAM_IMGSIZE_QUADVGA_V,
     CAM_IMAGE_PIX_FMT_JPG);

   //AviMJPEG myAviMjpeg(mySD, "test.avi");
   //aviMjpeg = &myAviMjpeg;

     outFile = mySD.open("test2.avi", O_WRONLY | O_BINARY | O_CREAT | O_TRUNC);
      if (!outFile)
      {
        Serial.println(F("File open failed"));
        while (1);
        return;
      }
      
   aviMjpeg = new AviMJPEG(outFile, "test2.avi");
   aviMjpeg->writeHeader();
}

void loopCamera() {
   if (video_frames_count >= 30 * 10) {
      // test mode: 60 frames
      return;
   }
    /* You can change the format of still picture at here also, if you want. */

  /* theCamera.setStillPictureImageFormat(
   *   CAM_IMGSIZE_HD_H,
   *   CAM_IMGSIZE_HD_V,
   *   CAM_IMAGE_PIX_FMT_JPG);
   */

  /* This sample code can take 100 pictures in every one second from starting. */

  if (take_picture_count < 100000)
    {

      /* Take still picture.
      * Unlike video stream(startStreaming) , this API wait to receive image data
      *  from camera device.
      */
  
      //Serial.println("call takePicture()");
      CamImage img = theCamera.takePicture();

      /* Check availability of the img instance. */
      /* If any error was occured, the img is not available. */

      if (img.isAvailable())
        {
          /* Create file name */
          video_frames_count++;
          if (video_frames_count < 30 * 2) {
            // write frame
            aviMjpeg->writeFrame(img.getImgBuff(), img.getImgSize());
            
          } else if (video_frames_count == 30 * 2 ) {
            // write end + close the file
            aviMjpeg->writeEnd();
            Serial.println("Video saved.");            
          }         
        }

      take_picture_count++;
    }
}

/******* GPS *******/
void setupGps() {
  setupGnssTracker();
}

void loopGps() {
  loopGnssTracker();
}

/******* AUDIO *******/

AudioClass *theAudio;
File audioFile;
bool ErrEnd = false;
bool endRecording = false;

/**
 * @brief Audio attention callback
 *
 * When audio internal error occurc, this function will be called back.
 */

static void audio_attention_cb(const ErrorAttentionParam *atprm)
{
  puts("Attention!");
  
  if (atprm->error_code >= AS_ATTENTION_CODE_WARNING)
    {
      ErrEnd = true;
   }
}

/**
 * @brief Setup recording of mp3 stream to file
 *
 * Select input device as microphone <br>
 * Initialize filetype to stereo mp3 with 48 Kb/s sampling rate <br>
 * Open "Sound.mp3" file in write mode
 */

static const int32_t recoding_frames = 400;
static const int32_t recoding_size = recoding_frames*288; /* 96kbps, 1152sample */

void setupAudio() {
  theAudio = AudioClass::getInstance();

  theAudio->begin(audio_attention_cb);

  puts("initialization Audio Library");

  /* Select input device as microphone */
  theAudio->setRecorderMode(AS_SETRECDR_STS_INPUTDEVICE_MIC, 210);

  /*
   * Initialize filetype to stereo mp3 with 48 Kb/s sampling rate
   * Search for MP3 codec in "/mnt/sd0/BIN" directory
   */
  theAudio->initRecorder(AS_CODECTYPE_MP3, "/mnt/sd0/BIN", AS_SAMPLINGRATE_48000, AS_CHANNEL_STEREO);
  puts("Init Recorder!");

  /* Open file for data write on SD card */
  audioFile = mySD.open("Sound.mp3", FILE_WRITE);
  /* Verify file open */
  if (!audioFile)
    {
      printf("File open error\n");
      exit(1);
    }

  theAudio->startRecorder();
  puts("Recording Start!");
}

void loopAudio() {
  if (endRecording) {
    return;
  }
  
  err_t err;
  /* recording end condition */
  if (theAudio->getRecordingSize() > recoding_size)
    {
      theAudio->stopRecorder();
      sleep(1);
      err = theAudio->readFrames(audioFile);

      goto exitRecording;
    }

  /* Read frames to record in file */
  err = theAudio->readFrames(audioFile);

  if (err != AUDIOLIB_ECODE_OK)
    {
      printf("File End! =%d\n",err);
      theAudio->stopRecorder();
      goto exitRecording;
    }

  if (ErrEnd)
    {
      printf("Error End\n");
      theAudio->stopRecorder();
      goto exitRecording;
    }

  /* This sleep is adjusted by the time to write the audio stream file.
     Please adjust in according with the processing contents
     being processed at the same time by Application.
  */
//  usleep(10000);

  return;

exitRecording:

  theAudio->closeOutputFile(audioFile);
  audioFile.close();
  
  theAudio->setReadyMode();
  theAudio->end();
  
  puts("End Recording");
  endRecording = true;
}

/******* USB *******/

void setupUsb() {
  if (mySD.beginUsbMsc()) {
    Serial.println("USB MSC Failure!");
  } else {
    Serial.println("*** USB MSC Prepared! ***");
    Serial.println("Insert SD and Connect Extension Board USB to PC.");
  }
}

void loopUsb() {
  
}

AVI MJPEG Encoder

C/C++
// Based on:
// https://github.com/ArduCAM/Arduino/blob/master/ArduCAM/examples/mini/ArduCAM_Mini_Video2SD/ArduCAM_Mini_Video2SD.ino
// (license: MIT)

#include <stdint.h>
#include <SDHCI.h>
#include <fcntl.h>
//#include <SD.h>

#define BUFFSIZE 512  // 512 is a good buffer size for SD writing. 4096 would be better, on boards with enough RAM (not Arduino Uno of course)
#define WIDTH_1 0x40 // Video width in pixel, hex. Here we set 320 (Big Endian: 320 = 0x01 0x40 -> 0x40 0x01). For 640: 0x80
#define WIDTH_2 0x01 // For 640: 0x02  
#define HEIGHT_1 0xF0 // 240 pixels height (0x00 0xF0 -> 0xF0 0x00). For 480: 0xE0
#define HEIGHT_2 0x00 // For 480: 0x01 
#define FPS 0x0F // 15 FPS. Placeholder: will be overwritten at runtime based upon real FPS attained

#define AVIOFFSET 240 // AVI main header length

/* Constants */

const uint8_t zero_buf[4] = {0x00, 0x00, 0x00, 0x00};
const uint8_t avi_header[AVIOFFSET] /*PROGMEM*/ = {
  0x52, 0x49, 0x46, 0x46, 0xD8, 0x01, 0x0E, 0x00, 0x41, 0x56, 0x49, 0x20, 0x4C, 0x49, 0x53, 0x54,
  0xD0, 0x00, 0x00, 0x00, 0x68, 0x64, 0x72, 0x6C, 0x61, 0x76, 0x69, 0x68, 0x38, 0x00, 0x00, 0x00,
  0xA0, 0x86, 0x01, 0x00, 0x80, 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
  0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x84, 0x00, 0x00, 0x00,
  0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x73,
  0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x01, 0x00, 0x00, 0x00, FPS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66,
  0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00,
  0x01, 0x00, 0x18, 0x00, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54,
  0x10, 0x00, 0x00, 0x00, 0x6F, 0x64, 0x6D, 0x6C, 0x64, 0x6D, 0x6C, 0x68, 0x04, 0x00, 0x00, 0x00,
  0x64, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x00, 0x01, 0x0E, 0x00, 0x6D, 0x6F, 0x76, 0x69,
};

const uint32_t JPEG_HEADER_SMALL_LENGTH = 6;
const uint8_t jpeg_avi_header_small[JPEG_HEADER_SMALL_LENGTH] = {
  0xff, 0xd8, 0x4a, 0x46, 0x49, 0x46,
};

static void inline print_quartet(uint32_t in, File fd){
  // Writes an uint32_t in Big Endian at current file position  
  uint32_t i = in;
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);  i = i >> 8;   //i /= 0x100;
  fd.write(i % 0x100);
  //fd.write((uint8_t)((i >> 24) & 0xff));
  //fd.write((uint8_t)((i >> 16) & 0xff));
  //fd.write((uint8_t)((i >> 8) & 0xff));
  //fd.write((uint8_t)((i >> 0) & 0xff));
  //Serial.print("Wrote 32: ");
  //Serial.println(in);
}

static void inline writeBuf(File &fd, const uint8_t *buf, size_t size) {
  size_t wrote;
  wrote = fd.write(buf, size);
  fd.flush();
  //Serial.print("Wrote buf: ");
  //Serial.println(wrote);
}

static void inline seekTo(File &fd, int32_t pos) {  
  //Serial.print("Seek to ");
  //Serial.print(pos);
  if (pos < 0) {
    Serial.print(" INVALID SEEK");
    return;
  }
  if (fd.seek(pos)) {
    //Serial.println(" OK");     
  } else {
    //Serial.println(" FAILED");
  }  
}

static uint32_t inline filePos(File &fd) {
  //Serial.print("File position: ");
  uint32_t fileposition = fd.position();
  //Serial.println(fileposition);
  return fileposition;
}

class AviMJPEG {
public:
    unsigned long movi_size = 0;

    File& outFile;

    byte buf[AVIOFFSET];
    char *filename;
    uint16_t frame_cnt = 0;
    unsigned long fileposition = 0;

    // for stats
    uint32_t uVideoLen = 0;
    uint32_t startms;
    uint32_t elapsedms;


    AviMJPEG(File &file, char* filename) : outFile(file), filename(filename) {
      Serial.print("AVI: File name will be ");
      Serial.println(filename);
    }

    void writeHeader() {
      Serial.println("AVI: write header");
      startms = millis();

      //Open the new file
      // Write AVI Main Header
      // Some entries will be overwritten later
      for (int i = 0; i < AVIOFFSET; i++)
      {
        char ch = pgm_read_byte(&avi_header[i]);
        buf[i] = ch;
      }

      writeBuf(outFile, buf, AVIOFFSET);
      outFile.flush();      
   }

   void writeFrame(byte *buf, uint32_t jpeg_size) {          
      Serial.print("AVI: write frame ");
      Serial.println(frame_cnt);
      // Spresense .JPG: ff d8 ff db 00 84
      // skip: ff d8
      jpeg_size -= 2;
      buf = buf + 2;
      
      // Write segment. We store 1 frame for each segment (video chunk)
      writeBuf(outFile, (uint8_t*) "00dc", 4); // "start of video data chunk" (00 = data stream #0, d = video, c = "compressed")
      writeBuf(outFile, zero_buf, 4);	// Placeholder for actual JPEG frame size, to be overwritten later

      // Wire frame
      writeBuf(outFile, jpeg_avi_header_small, JPEG_HEADER_SMALL_LENGTH);
      writeBuf(outFile, buf, jpeg_size);
      jpeg_size += JPEG_HEADER_SMALL_LENGTH;

      // Padding
      uint32_t remnant = jpeg_size & 0x00000001;	// Align to 16 bit: add 0 or 1 "0x00" bytes
      if (remnant > 0) {
        writeBuf(outFile, zero_buf, remnant); // see https://docs.microsoft.com/en-us/windows/desktop/directshow/avi-riff-file-reference
      }

      movi_size += jpeg_size;	// Update totals
      uVideoLen += jpeg_size;   // <- This is for statistics only

      // Now we have the real frame size in bytes. Time to overwrite the placeholder
  
      fileposition = filePos(outFile);  // Here, we are at end of chunk (after padding)
      seekTo(outFile, fileposition - jpeg_size - remnant - 4); // Here we are the the 4-bytes blank placeholder
      print_quartet(jpeg_size, outFile);    // Overwrite placeholder with actual frame size (without padding)
      //seekTo(outFile, fileposition - jpeg_size - remnant + 2); // Here is the FOURCC "JFIF" (JPEG header)
      //writeBuf(outFile, (uint8_t*) "AVI1", 4);         // Overwrite "JFIF" (still images) with more appropriate "AVI1"

      // Return to end of JPEG, ready for next chunk
      seekTo(outFile, fileposition);
      
      frame_cnt++;
   }

   void writeEnd() {
      Serial.println("AVI: write end");

      // Compute statistics
      elapsedms = millis() - startms;
      float fRealFPS = (1000.0f * (float)frame_cnt) / ((float)elapsedms);
      float fmicroseconds_per_frame = 1000000.0f / fRealFPS;
      uint8_t iAttainedFPS = round(fRealFPS); // Will overwrite AVI header placeholder
      uint32_t us_per_frame = round(fmicroseconds_per_frame); // Will overwrite AVI header placeholder

      //Modify the MJPEG header from the beginning of the file, overwriting various placeholders
      seekTo(outFile, 4);
      print_quartet(movi_size + 12 * frame_cnt + 4, outFile); //    riff file size
      //overwrite hdrl
      //hdrl.avih.us_per_frame:
      seekTo(outFile, 0x20);
      print_quartet(us_per_frame, outFile);
      unsigned long max_bytes_per_sec = movi_size * iAttainedFPS / frame_cnt; //hdrl.avih.max_bytes_per_sec
      seekTo(outFile, 0x24);
      print_quartet(max_bytes_per_sec, outFile);
      //hdrl.avih.tot_frames
      seekTo(outFile, 0x30);
      print_quartet(frame_cnt, outFile);
      seekTo(outFile, 0x84);
      print_quartet((int)iAttainedFPS, outFile);
      //hdrl.strl.list_odml.frames
      seekTo(outFile, 0xe0);
      print_quartet(frame_cnt, outFile);
      seekTo(outFile, 0xe8);
      print_quartet(movi_size, outFile);// size again
      
      //Close the file
      outFile.flush();
      outFile.close();

      /* Statistics */
      Serial.println(F("\n*** Video recorded and saved ***\n"));
      Serial.print(F("Recorded "));
      Serial.print(elapsedms / 1000);
      Serial.print(F("s in "));
      Serial.print(frame_cnt);
      Serial.print(F(" frames\nFile size is "));
      Serial.print(movi_size + 12 * frame_cnt + 4);
      Serial.print(F(" bytes\nActual FPS is "));
      Serial.print(fRealFPS, 2);
      Serial.print(F("\nMax data rate is "));
      Serial.print(max_bytes_per_sec);
      Serial.print(F(" byte/s\nFrame duration is "));
      Serial.print(us_per_frame);
      Serial.println(F(" us"));
      Serial.print(F("Average frame length is "));
      Serial.print(uVideoLen / frame_cnt);
      Serial.println(F(" bytes"));
   }
};

Arduino Project

Arduino
No preview (download only).

Credits

Attila Tőkés

Attila Tőkés

36 projects • 221 followers
Software Engineer experimenting with hardware projects involving IoT, Computer Vision, ML & AI, FPGA, Crypto and other related technologies.

Comments