namayatsuhashiのアイコン画像
namayatsuhashi 2022年09月26日作成 (2022年09月26日更新)
製作品 製作品 閲覧数 691
namayatsuhashi 2022年09月26日作成 (2022年09月26日更新) 製作品 製作品 閲覧数 691

路面の凸凹・障害物を記録するデバイス(自転車用)

路面の凸凹・障害物を記録するデバイス(自転車用)

概要
自転車で道を走っていると溝や凸凹した道が結構あります。中には転倒しかねないような大きな亀裂があったりもします。
そんな危険な箇所を予め知っておくことができると便利かなと思いました。慣れない道だと特に。
ということで、SPRESENSEを使って路面の危ない箇所を写真にとって、さらにGPSと紐付けて記録するデバイスを作ってみました。

機能
・自転車に取り付けます
・ボタンを押すと写真をとります
・写真を撮ったときのGPS情報および時刻情報を紐付けて保存します
・ELTRES通信で現在地情報を1分間隔でクラウドに送信します

パーツ構成
・Spresense メインボード
・Spresense 拡張ボード
・Spresense カメラボード(通常カメラ)
・ELTRESアドオンボード
・マイクロSDカード
・モバイルバッテリー
・LCD(Mic&LCD KIT for SPRESENSE)

キャプションを入力できます

動かしてみた
自転車に取り付けて、グルっと河川敷のあたりをまわってきました。
GPS情報はCLIP Viewer Liteから確認できます。問題なく取れてそうですね。
キャプションを入力できます

グラフ表示にしてみました。いい感じです。
キャプションを入力できます

危険な溝がある場所でボタンを押して写真を撮ってみました。
SDカードに写真が保存されているはずなので、家に帰ってパソコンで確認してみました。大丈夫そうですね。
ちなみにファイル名に撮影時間と緯度・経度情報を加えました。これで場所と写真を紐付けられそうです。
キャプションを入力できます

写真の中身を見てみます。いい感じに危険な溝が撮れています(笑)
キャプションを入力できます

ソースコード

#include <SDHCI.h>
#include <Camera.h>
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <EltresAddonBoard.h>

// *** ELTRES関連 ***
// PIN定義:LED(プログラム状態)
#define LED_RUN PIN_LED0
// PIN定義:LED(GNSS電波状態)
#define LED_GNSS PIN_LED1
// PIN定義:LED(ELTRES状態)
#define LED_SND PIN_LED2
// PIN定義:LED(エラー状態)
#define LED_ERR PIN_LED3

// プログラム内部状態:初期状態
#define PROGRAM_STS_INIT      (0)
// プログラム内部状態:起動中
#define PROGRAM_STS_RUNNING   (1)
// プログラム内部状態:終了
#define PROGRAM_STS_STOPPED   (3)

// プログラム内部状態
int program_sts = PROGRAM_STS_INIT;
// GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ
bool gnss_recevie_timeout = false;
// 点滅処理で最後に変更した時間
uint64_t last_change_blink_time = 0;
// イベント通知での送信直前通知(5秒前)受信フラグ
bool event_send_ready = false;
// ペイロードデータ格納場所
uint8_t payload[16];
// 最新のGGA情報
eltres_board_gga_info last_gga_info;

// *** LCD関連 ***
#define TFT_DC 9
#define TFT_CS -1
#define TFT_RST 8
#define TFT_ROTATION 3
#define BUTTON1 4
#define BUTTON2 5
#define BUTTON3 6
#define BUTTON4 7

Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST);
int btnPushed1 = 1;
int btnPushed2 = 1;
int btnPushed3 = 1;
int btnPushed4 = 1;

// *** SDカード関連 ***
SDClass theSD;
int               g_pict_id = 0;
int               g_width   = CAM_IMGSIZE_VGA_H;
int               g_height  = CAM_IMGSIZE_VGA_V;
CAM_IMAGE_PIX_FMT g_img_fmt = CAM_IMAGE_PIX_FMT_JPG;
CAM_WHITE_BALANCE g_wb      = CAM_WHITE_BALANCE_AUTO;
CAM_COLOR_FX      g_cfx     = CAM_COLOR_FX_NONE;
bool              g_awb     = true;
bool              g_ae      = true;
int               g_divisor = 7;

int take_picture_count = 0;

/****************************************************************************
 * Print error message
 ****************************************************************************/
void printError(enum CamErr err)
{
  Serial.print("Error: ");
  switch (err) {
  case CAM_ERR_NO_DEVICE:             Serial.println("No Device");                     break;
  case CAM_ERR_ILLEGAL_DEVERR:        Serial.println("Illegal device error");          break;
  case CAM_ERR_ALREADY_INITIALIZED:   Serial.println("Already initialized");           break;
  case CAM_ERR_NOT_INITIALIZED:       Serial.println("Not initialized");               break;
  case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break;
  case CAM_ERR_CANT_CREATE_THREAD:    Serial.println("Failed to create thread");       break;
  case CAM_ERR_INVALID_PARAM:         Serial.println("Invalid parameter");             break;
  case CAM_ERR_NO_MEMORY:             Serial.println("No memory");                     break;
  case CAM_ERR_USR_INUSED:            Serial.println("Buffer already in use");         break;
  case CAM_ERR_NOT_PERMITTED:         Serial.println("Operation not permitted");       break;
  default:
    break;
  }
}

/****************************************************************************
 * Callback from Camera library when video frame is captured.
 ****************************************************************************/
void CamCB(CamImage img)
{
  /* Check the img instance is available or not. */
  if (img.isAvailable()) {
      /* If you want RGB565 data, convert image data format to RGB565 */
      img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565);

      /* You can use image data directly by using getImgSize() and getImgBuff().
       * for displaying image to a display, etc. */
      tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), 320, 240);
  } else {
    Serial.println("Failed to get video stream image");
  }
}


/**
 * @brief イベント通知受信コールバック
 * @param event イベント種別
 */
void eltres_event_cb(eltres_board_event event) {
  switch (event) {
  case ELTRES_BOARD_EVT_GNSS_TMOUT:
    // GNSS電波受信タイムアウト
    Serial.println("gnss wait timeout error.");
    gnss_recevie_timeout = true;
    break;
  case ELTRES_BOARD_EVT_IDLE:
    // アイドル状態
    Serial.println("waiting sending timings.");
    digitalWrite(LED_SND, LOW);
    break;
  case ELTRES_BOARD_EVT_SEND_READY:
    // 送信直前通知(5秒前)
    Serial.println("Shortly before sending, so setup payload if need.");
    event_send_ready = true;
    break;
  case ELTRES_BOARD_EVT_SENDING:
    // 送信開始
    Serial.println("start sending.");
    digitalWrite(LED_SND, HIGH);
    break;
  case ELTRES_BOARD_EVT_GNSS_UNRECEIVE:
    // GNSS電波未受信
    Serial.println("gnss wave has not been received.");
    digitalWrite(LED_GNSS, LOW);
    break;
  case ELTRES_BOARD_EVT_GNSS_RECEIVE:
    // GNSS電波受信
    Serial.println("gnss wave has been received.");
    digitalWrite(LED_GNSS, HIGH);
    gnss_recevie_timeout = false;
    break;
  case ELTRES_BOARD_EVT_FAULT:
    // 内部エラー発生
    Serial.println("internal error.");
    break;
  }
}

/**
 * @brief GGA情報受信コールバック
 * @param gga_info GGA情報のポインタ
 */
void gga_event_cb(const eltres_board_gga_info *gga_info) {
  Serial.print("[gga]");
  last_gga_info = *gga_info;
  if (gga_info->m_pos_status) {
    // 測位状態
    // GGA情報をシリアルモニタへ出力
    Serial.print("utc: ");
    Serial.println((const char *)gga_info->m_utc);
    Serial.print("lat: ");
    Serial.print((const char *)gga_info->m_n_s);
    Serial.print((const char *)gga_info->m_lat);
    Serial.print(", lon: ");
    Serial.print((const char *)gga_info->m_e_w);
    Serial.println((const char *)gga_info->m_lon);
    Serial.print("pos_status: ");
    Serial.print(gga_info->m_pos_status);
    Serial.print(", sat_used: ");
    Serial.println(gga_info->m_sat_used);
    Serial.print("hdop: ");
    Serial.print(gga_info->m_hdop);
    Serial.print(", height: ");
    Serial.print(gga_info->m_height);
    Serial.print(" m, geoid: ");
    Serial.print(gga_info->m_geoid);
    Serial.println(" m");
  } else {
    // 非測位状態
    // "invalid data"をシリアルモニタへ出力
    Serial.println("invalid data.");
  }
}

void setup() {
  CamErr err;

  // put your setup code here, to run once:
  Serial.begin(115200);

  // LED初期設定
  pinMode(LED_RUN, OUTPUT);
  digitalWrite(LED_RUN, HIGH);
  pinMode(LED_GNSS, OUTPUT);
  digitalWrite(LED_GNSS, LOW);
  pinMode(LED_SND, OUTPUT);
  digitalWrite(LED_SND, LOW);
  pinMode(LED_ERR, OUTPUT);
  digitalWrite(LED_ERR, LOW);

  pinMode(BUTTON1, INPUT);
  pinMode(BUTTON2, INPUT);
  pinMode(BUTTON3, INPUT);
  pinMode(BUTTON4, INPUT);

  // ELTRES起動処理
  eltres_board_result ret = EltresAddonBoard.begin(ELTRES_BOARD_SEND_MODE_1MIN,eltres_event_cb, gga_event_cb);
  if (ret != ELTRES_BOARD_RESULT_OK) {
    // ELTRESエラー発生
    digitalWrite(LED_RUN, LOW);
    digitalWrite(LED_ERR, HIGH);
    program_sts = PROGRAM_STS_STOPPED;
    Serial.print("cannot start eltres board (");
    Serial.print(ret);
    Serial.println(").");
  } else {
    // 正常
    program_sts = PROGRAM_STS_RUNNING;
  }

  /* Initialize TFT */
  tft.begin();
  tft.setRotation(TFT_ROTATION);

  /* Initialize SD */
  while (!theSD.begin()) {
    /* wait until SD card is mounted. */
    Serial.println("Insert SD card.");
    sleep(1);
  }

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

  Serial.println("Prepare camera");
  err = theCamera.begin();
  if (err != CAM_ERR_SUCCESS) {
    printError(err);
  }

  /* Start video stream. */
  /*
  Serial.println("Start streaming");
  err = theCamera.startStreaming(true, CamCB);
  if (err != CAM_ERR_SUCCESS) {
    printError(err);
  }
  */

  /* Set parameters about still picture. */
  Serial.println("Set still picture format");
  err = theCamera.setStillPictureImageFormat(g_width, g_height,
                                             g_img_fmt, g_divisor);
  if (err != CAM_ERR_SUCCESS) {
    printError(err);
  }
}

void loop() {
  int btnState = digitalRead(BUTTON1);
  if (btnState == 0 && btnState != btnPushed1)
  {
    // ボタン1押下
    Serial.println("Button1 pushed.");

    if (last_gga_info.m_pos_status)
    {
      Serial.print("last utc: ");
      Serial.println((const char *)last_gga_info.m_utc);
      Serial.print("last lat: ");
      Serial.print((const char *)last_gga_info.m_n_s);
      Serial.print((const char *)last_gga_info.m_lat);
      Serial.print(",last lon: ");
      Serial.print((const char *)last_gga_info.m_e_w);
      Serial.println((const char *)last_gga_info.m_lon);

      Serial.println("call takePicture()");
      CamImage img = theCamera.takePicture();

      if (img.isAvailable())
      {
        /* Create file name */
        char filename[16] = {0};
        sprintf(filename, "PICT%s_%s%s_%s%s.JPG", (const char *)last_gga_info.m_utc, (const char *)last_gga_info.m_n_s, (const char *)last_gga_info.m_lat, (const char *)last_gga_info.m_e_w, (const char *)last_gga_info.m_lon);

        Serial.print("Save taken picture as ");
        Serial.print(filename);
        Serial.println("");

        /* Remove the old file with the same file name as new created file,
          * and create new file.
          */
        theSD.remove(filename);
        File myFile = theSD.open(filename, FILE_WRITE);
        myFile.write(img.getImgBuff(), img.getImgSize());
        myFile.close();
        
        take_picture_count++;
      }
      else
      {
        /* The size of a picture may exceed the allocated memory size.
          * Then, allocate the larger memory size and/or decrease the size of a picture.
          * [How to allocate the larger memory]
          * - Decrease jpgbufsize_divisor specified by setStillPictureImageFormat()
          * - Increase the Memory size from Arduino IDE tools Menu
          * [How to decrease the size of a picture]
          * - Decrease the JPEG quality by setJPEGQuality()
          */
        Serial.println("Failed to take picture");
      }
    }
    else
    {
      Serial.println("GPS not ready.");
    }
  }
  btnPushed1 = btnState;

  btnState = digitalRead(BUTTON2);
  if (btnState == 0 && btnState != btnPushed2)
  {
    // ボタン2押下
    Serial.println("Button2 pushed.");
  }
  btnPushed2 = btnState;

  btnState = digitalRead(BUTTON3);
  if (btnState == 0 && btnState != btnPushed3)
  {
    // ボタン3押下
    Serial.println("Button3 pushed.");
  }
  btnPushed3 = btnState;

  btnState = digitalRead(BUTTON4);
  if (btnState == 0 && btnState != btnPushed4)
  {
    // ボタン4押下
    Serial.println("Button4 pushed.");
  }
  btnPushed4 = btnState;


  switch (program_sts) {
    case PROGRAM_STS_RUNNING:
      // プログラム内部状態:起動中
      if (gnss_recevie_timeout) {
        // GNSS電波受信タイムアウト(GNSS受信エラー)時の点滅処理
        uint64_t now_time = millis();
        if ((now_time - last_change_blink_time) >= 1000) {
          last_change_blink_time = now_time;
          bool set_value = digitalRead(LED_ERR);
          bool next_value = (set_value == LOW) ? HIGH : LOW;
          digitalWrite(LED_ERR, next_value);
        }
      } else {
        digitalWrite(LED_ERR, LOW);
      }

      if (event_send_ready) {
        // 送信直前通知時の処理
        event_send_ready = false;
        setup_payload_gps();
        // 送信ペイロードの設定
        EltresAddonBoard.set_payload(payload);
      }
      break;
     
    case PROGRAM_STS_STOPPED:
      // プログラム内部状態:終了
      break;
  }
  // 次のループ処理まで100ミリ秒待機
  delay(100);

}

/**
 * @brief GPSペイロード設定
 */
void setup_payload_gps() {
  String lat_string = String((char*)last_gga_info.m_lat);
  String lon_string = String((char*)last_gga_info.m_lon);
  int index;
  uint32_t gnss_time;
  uint32_t utc_time;

  // GNSS時刻(epoch秒)の取得
  EltresAddonBoard.get_gnss_time(&gnss_time);
  // UTC時刻を計算(閏秒補正)
  utc_time = gnss_time - 18;

  // 設定情報をシリアルモニタへ出力
  Serial.print("[setup_payload_gps]");
  Serial.print("lat:");
  Serial.print(lat_string);
  Serial.print(",lon:");
  Serial.print(lon_string);
  Serial.print(",utc:");
  Serial.print(utc_time);
  Serial.print(",pos:");
  Serial.print(last_gga_info.m_pos_status);
  Serial.println();

  // ペイロード領域初期化
  memset(payload, 0x00, sizeof(payload));
  // ペイロード種別[GPSペイロード]設定
  payload[0] = 0x81;
  // 緯度設定
  index = 0;
  payload[1] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4)
                + lat_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  payload[2] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4)
                + lat_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  index += 1;   // skip "."
  payload[3] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4)
                + lat_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  payload[4] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4)
                + lat_string.substring(index+1,index+2).toInt()) & 0xff);
  // 経度設定
  index = 0;
  payload[5] = (uint8_t)(lon_string.substring(index,index+1).toInt() & 0xff);
  index += 1;
  payload[6] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4)
                + lon_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  payload[7] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4)
                + lon_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  index += 1;   // skip "."
  payload[8] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4)
                + lon_string.substring(index+1,index+2).toInt()) & 0xff);
  index += 2;
  payload[9] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4)
                + lon_string.substring(index+1,index+2).toInt()) & 0xff);
  // 時刻(EPOCH秒)設定
  payload[10] = (uint8_t)((utc_time >> 24) & 0xff);
  payload[11] = (uint8_t)((utc_time >> 16) & 0xff);
  payload[12] = (uint8_t)((utc_time >> 8) & 0xff);
  payload[13] = (uint8_t)(utc_time & 0xff);
  // 拡張用領域(0固定)設定
  payload[14] = 0x00;
  // 品質設定
  payload[15] = last_gga_info.m_pos_status;
}

}

あとがき
結果的にはサンプルコードを切り貼りしただけな感じになってしまいました。
最初は画像処理を使って溝の検知を目論んでいましたが、素人にはハードルが高く挫折。
次にジャイロセンサーを使って溝の検知も考えてましたが、基本的には溝は避けて通るから検知できない。
ということでシンプルにボタンを手動で押して写真を撮ることにしました。

1
ログインしてコメントを投稿する