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

Spresenseで夏場の車内子供置き去りをなくす

Spresenseで夏場の車内子供置き去りをなくす

車内置き去りによる死亡事件が毎年発生している

2022年は川崎幼稚園の3歳園児バス置き去り事件が記憶に新しい。
https://www.asahi.com/articles/ASQ9G61NDQ9GUTIL013.html

この様な痛ましい事件が毎年のように起こっていることから、センサーを活用して車内に子供が置き去りにされていることを検知し、車のそばにいない家族や先生には通知で警告するとともに、アラームで車の周りの人に存在を知らせることができれば、多くの子供たちの命を守ることができると考え、今回作成するシステムを提案する。

なおこのシステムは車の天井に設置する想定である。

システムの実現方法

GPSの情報に更新がない時(=移動が⾒受けられない時)、かつ⾞内の温度が⼀定以上になったときに置き去りの可能性があると考える。
また加速度センサー、CO2センサーを使⽤し、加速度センサーでは⾞内の揺れで⼈の動きを検出するほか、CO2センサーでの濃度の急激な上昇から⾞内の広い範囲における⼈の存在を検知する。これら2つのセンサーの反応を組み合わせることで誤作動によるアラームを防止する。

ELTRESを使⽤してクラウド上に検出結果をアップロードし、未来の危険性を防ぐために継続的に気温や湿度を観察し、グラフで観測できるようにする。

近くにいる⼈向けには⾞内に置き去りにされていることを気づいてもらえるようアラームを鳴らし、遠くにいる⼈向けには、LINEなどに通知を送り、⾞内の温度の情報と警告を伝える。(今回未実装)

使用した機材

SONY SPRESENSE メインボード
SONY SPRESENSE 拡張ボード
Sony Spresense 用 CO2センサー(SCD40) Addon ボード
SPRESENSE用ELTRESアドオンボード
MPU6050 3軸ジャイロスコープ・3軸加速度センサー モジュール
GPSアンテナ
ジャンパワイヤ赤黒黄白
ブレッドボード

システム構成

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

配線詳細

Spresense SCL -> MPU6050 SCL (ジャンパワイヤ黄)
Spresense SDA -> MPU6050 SDA(ジャンパワイヤ白)
Spresense Vout(5V) -> MPU6050 VCC(ジャンパワイヤ赤)
Spresense GND -> MPU6050 GND(ジャンパワイヤ黒)
拡張ボードのピンソケットの動作電圧はジャンパ位置を3.3V側に固定した。

ブレッドボード

Spresense

仮説設定・検証

自家用車がなく実験し辛いかったことから、代わりに自宅内にある密閉できる狭い部屋としてトイレを活用して実験を行った。ドアを閉めると大きくCO2の濃度が上がったことから、人の存在を検知できると考える。今回の実験結果よりCO2濃度が3000以上の時を対象に人がいる可能性が高いと仮定して検出することとする。
キャプションを入力できます

次に椅子に加速度センサーを置き、人の存在でどの程度X, Y, Zそれぞれの値が変わるかを検出した。動きがあるときと人がいない時を比較すると、動きがあるときはおおよそ0.20以内で値の変動があることに対し、人がいないときは0.01以内と値の変動がほとんどなかった。よって一定時間内の値の変動が0.10以上の時は人が車内にいると仮定することとする。
人がいないとき

立ち座りを繰り返している時

コード

CLIP Viewer Liteのコンテンツページより

  • センサ系サンプルプログラム内のSCD41搭載CO2/温度/湿度センサプログラム
  • ELTRESアドオンボード用サンプルプログラム内の位置情報ペイロード送信プログラム

「とある科学の忘備録」という名称のHPより、MPU6050のサンプルプログラム

のコードを参考にさせていただいた。

#include <Arduino.h>
#include <SensirionI2CScd4x.h>
#include <Wire.h>
#include <EltresAddonBoard.h>

// 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_STOPPING  (2)
// プログラム内部状態:終了
#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;
// イベント通知でのアイドル状態受信フラグ
bool event_idle = false;

// 送信回数
int send_count = 0;
uint8_t payload[16];
SensirionI2CScd4x scd4x;

void printUint16Hex(uint16_t value) {
    Serial.print(value < 4096 ? "0" : "");
    Serial.print(value < 256 ? "0" : "");
    Serial.print(value < 16 ? "0" : "");
    Serial.print(value, HEX);
}

void printSerialNumber(uint16_t serial0, uint16_t serial1, uint16_t serial2) {
    Serial.print("Serial: 0x");
    printUint16Hex(serial0);
    printUint16Hex(serial1);
    printUint16Hex(serial2);
    Serial.println();
}

/**
 * @brief イベント通知受信コールバック
 */
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);
    event_idle = true;
    break;
  case ELTRES_BOARD_EVT_SEND_READY:
    // 送信直前通知(5秒前)
    Serial.begin(115200);
    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情報受信コールバック
 */
void gga_event_cb(eltres_board_gga_info *gga_info) {
//  Serial.print("[gga]");
  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() {
  Serial.begin(115200);
  while (!Serial) {
      delay(100);
  }

  // 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);

  // 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;
  }

  Wire.begin();

  uint16_t error;
  char errorMessage[256];

  scd4x.begin(Wire);

  // stop potentially previously started measurement
  error = scd4x.stopPeriodicMeasurement();
  if (error) {
    Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  }

  uint16_t serial0;
  uint16_t serial1;
  uint16_t serial2;
  error = scd4x.getSerialNumber(serial0, serial1, serial2);
  if (error) {
      Serial.print("Error trying to execute getSerialNumber(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
  } else {
      printSerialNumber(serial0, serial1, serial2);
  }

  // Start Measurement
  error = scd4x.startPeriodicMeasurement();
  if (error) {
      Serial.print("Error trying to execute startPeriodicMeasurement(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
  }

  Serial. println("Waiting for first measurement... (5 sec)");
}

void loop() {
  uint32_t gnss_time;
  int32_t remaining;
  uint16_t error;
  char errorMessage[256];
  get_accs();

  // Read Measurement
  uint16_t co2 = 0;
  float temperature = 0.0f;
  float humidity = 0.0f;
  float co2_f = float(co2);
  bool isDataReady = false;
  error = scd4x.getDataReadyFlag(isDataReady);
  if (error) {
      Serial.print("Error trying to execute readMeasurement(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
      return;
  }
  if (!isDataReady) {
      return;
  }
  error = scd4x.readMeasurement(co2, temperature, humidity);
  if (error) {
      Serial.print("Error trying to execute readMeasurement(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
  } else if (co2 == 0) {
      Serial.println("Invalid sample detected, skipping.");
  } else if (co2 >= 3000 && temperature >= 30.0) {
    // TODO:「上記に位置情報が変化しておらず、加速度センサーが反応している場合」という条件を追加する(実装間に合わず)
      Serial.print("Please help children !!!");
      return;
  } else {
      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) {
          Serial.print("'send_ready");

          into_payload(temperature, humidity);
               
          // 送信直前通知時の処理
          event_send_ready = false;
          // 送信ペイロードの設定
          EltresAddonBoard.set_payload(payload);
          Serial.print("Co2:");
          Serial.print(co2);
          Serial.print("\t");
          Serial.print("Temperature:");
          Serial.print(temperature);
          Serial.print("\t");
          Serial.print("Humidity:");
          Serial.println(humidity);
        }
        
        if (event_idle) {
          // 送信完了時の処理
          event_idle = false;
          // GNSS時刻(epoch秒)の取得
          EltresAddonBoard.get_gnss_time(&gnss_time);
          Serial.print("gnss time: ");
          Serial.print(gnss_time);
          Serial.println(" sec");
          // 次送信までの残り時間の取得
          EltresAddonBoard.get_remaing_time(&remaining);
          Serial.print("remaining time: ");
          Serial.print(remaining);
          Serial.println(" sec");
        }
        break;
        
      case PROGRAM_STS_STOPPING:
        // プログラム内部状態:終了処理中
        // ELTRES停止処理(注意:この処理を行わないとELTRESが自動送信し続ける)
        EltresAddonBoard.end();
        digitalWrite(LED_RUN, LOW);
        digitalWrite(LED_GNSS, LOW);
        program_sts = PROGRAM_STS_STOPPED;
        break;
        
      case PROGRAM_STS_STOPPED:
        // プログラム内部状態:終了
        break;
    }
  }
  // 次のループ処理まで100ミリ秒待機
  delay(100);
}

void into_payload(float temperature, float humidity) {
  union {
    float f;
    uint32_t i;
  } raw;
  raw.f = temperature;

  payload[0] = 0x82;

  // 気温設定
  payload[1] = (uint8_t)(raw.i >> 24 & 0xff);
  payload[2] = (uint8_t)(raw.i >> 16 & 0xff);
  payload[3] = (uint8_t)(raw.i >> 8 & 0xff);
  payload[4] = (uint8_t)(raw.i >> 0 & 0xff);

  raw.f = humidity;

    // 湿度設定
  payload[5] = (uint8_t)(raw.i >> 24 & 0xff);
  payload[6] = (uint8_t)(raw.i >> 16 & 0xff);
  payload[7] = (uint8_t)(raw.i >> 8 & 0xff);
  payload[8] = (uint8_t)(raw.i >> 0 & 0xff);
}

void get_accs() {
  Wire.beginTransmission(0x68);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 14, true);
  while (Wire.available() < 14);
  int16_t axRaw, ayRaw, azRaw;

  axRaw = Wire.read() << 8 | Wire.read();
  ayRaw = Wire.read() << 8 | Wire.read();
  azRaw = Wire.read() << 8 | Wire.read();

  // 加速度値を分解能で割って加速度(G)に変換する  
  float new_acc_x = axRaw / 16384.0; //FS_SEL_0 16,384 LSB / g
  float new_acc_y = ayRaw / 16384.0;
  float new_acc_z = azRaw / 16384.0;

  Serial.print("x:");  Serial.print(new_acc_x);  Serial.print(",");
  Serial.print("y:");  Serial.print(new_acc_y);  Serial.print(",");
  Serial.print("z:");  Serial.print(new_acc_z);  Serial.print(",");
  Serial.print('\n');
}

ELTRESでの送信情報

下記のように温度が記録され、車内の温度の様子を継続的に観察することができる。湿度も熱中症予防のための暑さ指数の変数として使用できるため、継続的に観察できるようにELTRESで送信している。これらのグラフはhttps://clip-viewer-lite.com/ にて確認できる。

温度

湿度

注意点

MPU6050のピンヘッダ、もしくはジャンパワイヤの接触不良の場合、I2C通信に失敗し、Wire.requestFrom()メソッドでのエラーである「ERROR: Failed to read from i2c (errno = 0)」がシリアルモニタ上に連発するので気をつけたい。

中でもピンヘッダの接触不良の場合が多々あるので、接続時にMPU6050に赤いLEDが点灯し、通電していることを確認するほか、はんだ付けでピンヘッダを固定することが望ましい。

今後の展望

親が一緒に車に乗っている場合などの例外を検知できるように、親のスマホのGPSと連携させ、一定距離以上離れている時のみ通知とアラームを鳴らすなどの改良を加えたい。また実際の車内で実験して正しい閾値を設定するほか、通知とアラームを実装するほか、暑さ指数内における変数である「湿度」も考慮して通知の閾値を検討したい。

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