記事の概要
GPSロガーを作成したので、備忘録として記録を残します。
自分用のメモなので自明なことは省略していますが、不明点があればお気軽に本記事コメント欄にてご質問ください。
システム概要
通信モジュールEC21-J Mini PCIeにUART通信ケーブルでマイコンと接続し、マイコンでGPSデータを取得します。
GPSデータはGoogle Cloud Platform(以下、「GCP」と略)に記録します。
サーバとの通信はSORACOM IoT Simを用いてMQTT通信を行います。
環境構築
MQTT通信を行うためには以下の準備をしないといけません。
- 通信モジュールの購入
- SORACOM SIMの購入と設定
- GCPとの契約と設定
上記の通信環境構築方法は、以下の記事にて行っているのでご参照ください。
EC21-JとSORACOM IoT SIMによりGoogle IoT CoreでMQTT接続を行うまでの全過程の記録
また、今回はマイコンも必要になります。
- マイコンとGPSの購入
- マイコンと通信モジュールのUART通信ケーブル
マイコンには太陽誘電のBLEモジュールnRF52480を使用しました。
UART通信が使えるマイコンならば何でもかまいません。
将来的にスマホとのBLE通信を行いたかったので、BLEモジュールを使用しました。
また、マイコンと通信モジュール間でUART通信するために以下のモジュールで通信ケーブルを作成します。
マイコンのTX端子とRX端子をこのモジュールにハンダ付けするだけの簡単な工作になります。
極小RS232-TTLコンバータモジュール- Dサブ9ピンオスコネクタ(ケースキット付き)
GPSはEC21-JにGNSSが内蔵されているのならば不要になります。
私は複数のGPSを比較したかったので、以下も購入してみました。
Qwiic - NEO-M8U搭載 推測航法GPSモジュール
M8Uにはアンテナも必要です。
GPSアンテナ 28dB 5m SMAコネクタ型
SMAからU.FLに変換するケーブル
もしくは
Molex フレキシブルGNSS U.FLアンテナ(粘着テープ付き)
などを使用します。
UART端子はLTE通信モジュールとのUART通信に使用していたので、上記のGPSモジュールとはI2C通信しました。
M8UはI2C通信に対応していたので問題なかったのですが、GMS6-CR6はUART通信しかできないので、I2C通信をUART通信に変換するモジュールをマイコンとの間に挟んで使用しました。
SC16IS750 シリアル-I2C ピッチ変換済みモジュール
以下が実際に作成した治具になります。
この治具では、デバッグ用にSDカードにGPS測定データを記録しています。
性能試験が目的なので小型化は考えていませんでしたが、実用に使うならば基板を作成した方がいいと思います。
例えば、以下が参考になるかと思います。
オープンソースデータを流用して通信モジュールを作成する事例が紹介されています。
ESP8266 (ESP-WROOM-02) を SORACOM SIM と通信モジュール UC20-G を使って3G でインターネットに繋ぐ
ファームウェア
ファームウェアはATコマンドをUART送信して、結果をUART受信するだけの単純なプログラムになります。
ネットワークに接続後、MQTT通信により、GPSデータをGCPへPublishします。
使用するATコマンドは以下をご参照ください。
【作業メモ】Quectel社のLTE通信モジュールEC21の使用方法:MQTT通信
参考までに、私の作成したネットワークに接続するファームウェアの一部を以下に羅列します。
ATコマンドを順番に送るだけの単純なプログラムになります。
送信コマンドの結果を受信待機している間にも他の処理を実行できるように、送信と受信を分けています。
ec_protocol_open関数を繰り返し実行し続けることで、順番にコマンドを送受信しています。
ec_protcol_num = PROTOCOL_OPEN;
int main(void)
{
for (;;)
{
ec_exe_protocol();
}
}
void ec_exe_protocol (void)
{
uint8_t check_result;
if(PROTOCOL_NONE != ec_protcol_num)
{
ec_select_prtocol();
check_result = ec_check_response();
if(UART_SUCCESS == check_result)
{
//Timer Reset
sys_timer_count[RECEIVE_FAIL_TIMER] = 0;
//Negate PROCESS_WAIT_RESPONSE bit
ec_process_num = ec_process_num & ~PROCESS_WAIT_RESPONSE;
//Proceed to the next process
ec_process_num += 1;
} else if(UART_FAIL == check_result)
{
//Negate PROCESS_WAIT_RESPONSE bit and Redo the process
ec_process_num = ec_process_num & ~PROCESS_WAIT_RESPONSE;
}
// TimeOver
if(true == sys_timer_limit[RECEIVE_FAIL_TIMER])
{
ec_reset_protocol();
}
}
}
void ec_select_prtocol (void)
{
switch(ec_protcol_num)
{
case PROTOCOL_NONE:
break;
case PROTOCOL_ATE0:
ec_protocol_ate0();
break;
case PROTOCOL_OPEN:
ec_protocol_open();
break;
case PROTOCOL_CLOSE:
ec_protocol_close();
break;
case PROTOCOL_GPSON:
ec_protocol_gpson();
break;
case PROTOCOL_GPSOFF:
ec_protocol_gpsoff();
break;
case PROTOCOL_XTRA:
ec_protocol_xtra();
break;
case PROTOCOL_GPSGET:
ec_protocol_gpsget();
break;
case PROTOCOL_PUB_GPS:
ec_protocol_publish_gps();
break;
default:
break;
}
}
static void ec_protocol_open (void)
{
// Do nothing as long as the PROCESS_WAIT_RESPONSE bit is asserted
switch(ec_process_num)
{
case 0:
ec_start_protocol();
uint8_t qicsgp_on[] ="AT+QICSGP=1,1,\"soracom.io\",\"sora\",\"sora\",0\r\n";
send_message(&qicsgp_on[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_OK;
break;
case 1:
ec_buf_clear();
uint8_t qiact_check[] ="AT+QIACT?\r\n";
send_message(&qiact_check[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_QIACT;
break;
case 2:
ec_buf_clear();
uint8_t qiact_on[] ="AT+QIACT=1\r\n";
send_message(&qiact_on[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_OK;
break;
case 3:
ec_buf_clear();
uint8_t qmtopen[] = "AT+QMTOPEN=0,\"beam.soracom.io\",1883\r\n";
send_message(&qmtopen[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_QMTOPEN;
break;
case 4:
ec_buf_clear();
uint8_t qmtconnect_check[] ="AT+QMTCONN?\r\n";
send_message(&qmtconnect_check[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_QMTCONN;
break;
case 5:
ec_buf_clear();
uint8_t qmtconnect[] = "AT+QMTCONN=0,\"projects/vigilant-router-xxxxxx/locations/asia-east1/registries/xxxx/devices/SOMENAME\"\r\n";
send_message(&qmtconnect[0]);
ec_process_num |= PROCESS_WAIT_RESPONSE;
ec_check_num = CHECK_OK;
break;
case 6:
ec_reset_protocol();
break;
default:
break;
}
}
uint8_t ec_check_response (void)
{
uint8_t next_check = 0;
uint8_t utc_start = 0;
switch(ec_check_num)
{
case CHECK_OK:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
return UART_SUCCESS;
}
}
break;
case CHECK_QIACT:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=4; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-4] == 'Q' && receive_data[i-3] == 'I' && receive_data[i-2] == 'A' && receive_data[i-1] == 'C' && receive_data[i] == 'T')
{
ec_process_num += 1;
break;
}
}
return UART_SUCCESS;
}
break;
case CHECK_QMTOPEN_QUESTION:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=6; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-6] == 's' && receive_data[i-5] == 'o' && receive_data[i-4] == 'r' && receive_data[i-3] == 'a' && receive_data[i-2] == 'c' && receive_data[i-1] == 'o' && receive_data[i] == 'm')
{
ec_process_num += 1;
break;
}
}
return UART_SUCCESS;
}
break;
case CHECK_QMTOPEN:
for(int i=6; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-6] == 'Q' && receive_data[i-5] == 'M' && receive_data[i-4] == 'T' && receive_data[i-3] == 'O' && receive_data[i-2] == 'P' && receive_data[i-1] == 'E' && receive_data[i] == 'N')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
return UART_SUCCESS;
}
}
return UART_FAIL;
}
break;
case CHECK_QMTCONN:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=6; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-6] == 'Q' && receive_data[i-5] == 'M' && receive_data[i-4] == 'T' && receive_data[i-3] == 'C' && receive_data[i-2] == 'O' && receive_data[i-1] == 'N' && receive_data[i] == 'N')
{
ec_process_num += 1;
break;
}
}
return UART_SUCCESS;
}
break;
case CHECK_CONNECT:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=6; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-6] == 'C' && receive_data[i-5] == 'O' && receive_data[i-4] == 'N' && receive_data[i-3] == 'N' && receive_data[i-2] == 'E' && receive_data[i-1] == 'C' && receive_data[i] == 'T')
{
return UART_SUCCESS;
}
}
return UART_FAIL;
}
break;
case CHECK_QHHTPGET:
for(int i=7; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-7] == 'Q' && receive_data[i-6] == 'H' && receive_data[i-5] == 'H' && receive_data[i-4] == 'T' && receive_data[i-3] == 'P' && receive_data[i-2] == 'G' && receive_data[i-1] == 'E' && receive_data[i] == 'T')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
return UART_SUCCESS;
}
}
return UART_FAIL;
}
break;
case CHECK_QHTTPREADFILE:
for(int i=12; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-12] == 'Q' && receive_data[i-11] == 'H' && receive_data[i-10] == 'H'
&& receive_data[i-9] == 'T' && receive_data[i-8] == 'P' && receive_data[i-7] == 'R'
&& receive_data[i-6] == 'E' && receive_data[i-5] == 'A' && receive_data[i-4] == 'D'
&& receive_data[i-3] == 'F' && receive_data[i-2] == 'I' && receive_data[i-1] == 'L'
&& receive_data[i] == 'E' )
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
return UART_SUCCESS;
}
}
return UART_FAIL;
}
break;
case CHECK_UTC:
for(int i=3; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-3] == 'Q' && receive_data[i-2] == 'L' && receive_data[i-1] == 'T' && receive_data[i] == 'S')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=0; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i] == '\"')
{
utc_data[0] = receive_data[i];
utc_start = i+1;
next_check = 2;
break;
}
}
}
if (2 == next_check)
{
for(int j=utc_start; j<RECEIVE_BUF_MAX; j++)
{
utc_data[j-utc_start+1] = receive_data[j];
if(receive_data[j] == '\"')
{
utc_data[j-utc_start+2] = '\0';
return UART_SUCCESS;
}
}
}
break;
case CHECK_GPS:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == 'O' && receive_data[i] == 'K')
{
next_check = 1;
}
}
if(1 == next_check)
{
for(int i=2; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-2] == 'G' && receive_data[i-1] == 'P' && receive_data[i] == 'S')
{
calc_gps_message();
return UART_SUCCESS;
}
}
return UART_FAIL;
}
break;
case CHECK_PUBLISH:
for(int i=0; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i] == '>')
{
return UART_SUCCESS;
}
}
break;
case CHECK_RECEPTION:
for(int i=1; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-1] == '\r' && receive_data[i] == '\n')
{
return UART_SUCCESS;
}
}
break;
default:
break;
}
for(int i=4; i<RECEIVE_BUF_MAX; i++)
{
if(receive_data[i-4] == 'E' && receive_data[i-3] == 'R' && receive_data[i-2] == 'R' && receive_data[i-1] == 'O')
{
return UART_FAIL;
}
}
return UART_NONE;
}
GCPの設定
GCPを設定して、以下のフローを実行します。
GCPでは以下の設定が必要です。
- GCPアカウント作成
- IoT Coreの設定
- レジストリID
- リージョン
- Cloud Pub/Subトピック
- 選択するトピックがなければ、トピックの作成
- プロトコルの設定はMQTT
- 公開鍵・秘密鍵の作成
- デバイスの作成
- SORACOM側の設定
- Pub/Subの確認
- BigQueryの設定
- データセット作成
- テーブル作成
- Storageの設定
- バケットの作成
- Dataflowの設定
- BigQueryにエクスポート
- ジョブ作成
以下の記事がとても参考になりました。
「SORACOM側の設定」までは以下の記事をご参照ください。
EC21-JとSORACOM IoT SIMによりGoogle IoT CoreでMQTT接続を行うまでの全過程の記録:4. Google IoT Core
「Pub/Subの確認」以降の作業を以下に記録しますが、「GCPのほうのIoTCoreを使ってみる」の内容をなぞるだけなので、そちらを見た方が分かりやすいと思います。
Pub/Subの確認
メニューからPub/Subを選択し、IoT Coreの設定で作成したトピック「test」があることを確認します。
ちなみに「メッセージ表示」からPublishしたデータを確認できます。
まず、サブスクリプションを作成していなければ、作成してください。
例えば以下のように通信モジュールからデータをPublishしたとします。
AT+QMTPUB=0,0,0,0,"/devices/MatsuiEC21J/events"
{"Hello":"World1"}
(Ctrl-Zを押さないとデータ送信されないので、ご注意ください)
するとメッセージ表示画面でPULLをクリックすると、Publishしたデータが確認できます。
BigQueryの設定
データセット作成
メニューからBigQueryを選択し、リソースからプロジェクト名を選択して、データセットを作成をクリックします。
データセット「gps」を作成します。
テーブル作成
データセット「gps」を選択して、+マークをクリックしてテーブルを作成します。
テーブル名とフィールドを設定します。フィールドを追加するごとに、テーブルの列が増えます。
「+フィールドを追加」でフィールドの名前、型とモードを設定してください。
今回は以下のようなテーブル「gps_ueno」を作成しました。
仕事で上野・浅草に行く機会があったので、そこでGPS測定試験をすることにしました。
Storageの設定
データを一時保存するStorageの設定をします。
メニューからStorageを選択し、「バケットを作成」をクリックします。
バケットの作成
バケットの名前とデータ保存場所を設定します。
私はMulti-Regionに設定しましたが、実際はRegionで十分だと思います。
作成したバケット内にフォルダを作成します。
Dataflowの設定
Storageに蓄積したデータをBigQueryのテーブルに保存するためにはDataFlowを利用します。
BigQueryにエクスポート
Pub/Subのトピックの「...」から「エクスポート先」を「BigQuery」を選択します。
他と重複しないジョブ名を設定し、リージョンポイントを選択します。
必須パラメータはトピックとテーブルと一時保存場所です。
トピックはPub/Subで選択したトピックを入力します。
テーブルは先ほどに作成したテーブルを選択します。
一時保存場所は、バケットで作成したフォルダを選択します。
実行をクリックすればジョブの実行が開始します。
ジョブ実行
ジョブの実行に成功すると以下のような画面が表示されます。
ここで、マイコンでGPSデータを取得し、以下のコマンドを送信したとします。(コマンドの最後には、Ctrl-Zとして0x1aを送信します。)
AT+QMTPUB=0,0,0,0,"/devices/MatsuiEC21J/events"
{"gps_name":"EC21J","utc":"71632.0","latitude":"35.708255","longitude":"139.782183"}
するとBigQueryのテーブル「gps_ueno」にデータが保存されていることが確認できます。
試験終了後はジョブを停止するのを忘れないようにお気をつけください。
私はこのジョブの停止を忘れて1週間放置したせいで、無料3万円分の内の1万円ほどが消えていました。
未転送のデータがある場合は、「ドレイン」の設定にして、「STOP JOB」をクリックして終了します。
GPSログの確認
本来ならGPSデータをリアルタイムにスマホアプリに表示したいところですが、今回はPCにテーブルデータをダウンロードして、GoogleMapに反映させる方法を採用します。
Google Mapのサイドメニューからマイプレイスを選びます。
GPSデータの軌跡がマップ上に表示されます
参照
-
GCPのほうのIoTCoreを使ってみる
- 本記事のGCPにおける設定はAkijinさんの記事に倣っています。
- Data Science on the Google Cloud Platform
- Google Cloud Platform での IoT 分析パイプラインの構築
-
IoT in the Google Cloud
- GoogleのCloud Trainingです。実行するには課金が必要ですが、読むだけなら無料です。手順だけを参考にするならば、それで十分だと思われます。
-
【作業メモ】Quectel社のLTE通信モジュールEC21の使用方法
- 私の作成した記事になります。LTE通信モジュールのATコマンドやGCPの設定について解説しています。
- EC21-JとSORACOM IoT SIMによりGoogle IoT CoreでMQTT接続を行うまでの全過程の記録