RaspberryPi で、GPS Logger & Tracker の製作

## 概要
車で旅に出る時に軌跡を残しておきたいなと思って、スマホのアプリで軌跡を残していましたが、ちょっとGPSのデータを取得してみたい。実際のGPSのデータってどうなってんの？が今回の作業のきっかけ。
Google MyMapで、GPSロガーのデータをインポートすればKMLに変換してくれて表示してくれるけど、ちょっと自分でやってみよう！

## メインホスト
今、手元にRaspberryPi4Bが3台あります。
メインホストはこれを選択。

## GPSの購入
RasberryPi HAT形式のGPSを考えていましたが、遊ぶにはちょっとお高めなので、USB接続のGPSレシーバーを購入。
[Amazon GPSレシーバー GPS受信機 DOCTORADIO GR-7BN アンテナ内蔵 チップ一体型 USB出力 測位 時刻較正用](https://www.amazon.co.jp/PERSPECTIVE-GPSレシーバー-GPS受信機-GR-7BN-アンテナ内蔵/dp/B082Y1GLQK/ref=sr_1_2_sspa?__mk_ja_JP=カタカナ&dchild=1&keywords=GPSレシーバー&qid=1621474130&s=electronics&sr=1-2-spons&psc=1&spLa=ZW5jcnlwdGVkUXVhbGlmaWVyPUFXRlhXNVdXNUZHQ0smZW5jcnlwdGVkSWQ9QTAxNjExOTAzVTFaOUk2UVAyOEpCJmVuY3J5cHRlZEFkSWQ9QTI5SDFORTFNVEdTU1gmd2lkZ2V0TmFtZT1zcF9hdGYmYWN0aW9uPWNsaWNrUmVkaXJlY3QmZG9Ob3RMb2dDbGljaz10cnVl)

## 構成
1. RaspberryPi4にUSBでGPSレシーバーと繋ぐ
2. USBを介してシリアルでGPSレシーバからNMEAフォーマットのデータを受信する
3. シリアルから1行づつデータを、nodejsで組んだプログラムが読み込む
4. 読み込んだデータをLocalStorageに書き込む（ファイル）
5. 同時にmosquittoのMQTT Pub/SubにPublishする
6. WebBrowserで、MQTT Pub/SubをWebsocketsで受信するためにSubscribeしておく
7. WebBrowserで受信したデータをパースして、10進の緯度経度に変換して、GoogleMapにプロットする
8. 位置データをPolylineで結んで軌跡を表示させる

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4632/d83ae7c9-5d0d-4c5a-5027-21dce01a5886.png" width="580px">

## GPSデータの受信と保存
GPSのデータはシリアルポートから受信する。
GPSレシーバーをUSBのここに刺すと、`/dev/ttyACM0` で読むことができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4632/80a57008-a268-fa15-44cf-bd7f3486efe4.png)

取得対象はGPRMCのみとする。
下記のreadDataメソッドのdata変数をconsole.logで出力すれば内容を確認できます。
GPSデータのNMEAフォーマットは？に関しては、以下を参照してください。
[GPSのNMEAフォーマット](https://www.hiramine.com/physicalcomputing/general/gps_nmeaformat.html)

GPSデータではGPRMCのみ対象としています。
取得した内容をファイルに書き込んでいます。

```gps.js

// 日付モジュール
const moment = require('moment-timezone');

// ファイル操作モジュール
const fs = require('fs');

// パス操作モジュール
const path = require('path');

// シリアルからGPSデータ受信
const SerialPort = require('serialport');
const Readline = SerialPort.parsers.Readline;
const port = new SerialPort('/dev/ttyACM0');
const parser = new Readline();
port.pipe(parser);
parser.on('data', readData);

// MQTTモジュール
const mqttHost = 'localhost';
const mqtt = require('mqtt');
const mqttClient  = mqtt.connect('mqtt://'+mqttHost);
mqttClient.on('connect', function() {
	console.log('mqtt ' + mqttHost + ' connected.');
});
mqttClient.on('error', function(err) {
	console.log(err);
});

/**
 * GPSデータの読み込み
 *
 * @param data String GPSデータ
 * @return void
 */
function readData(data)
{
	// GPRMCのみ採用して書き込む
	if (data.match(/^\$GPRMC/, data) != null) {
		// ファイルに書き込み
		writeLogToFile(data);
		// MQTTでpublish
		mqttClient.publish('gps', data);
	}
}
/**
 * GPSデータをログに書き込む
 *
 * @param data String GPSデータ
 * @return void
 */
function writeLogToFile(data)
{
	// 書き込むファイルパスの取得
	let filepath = getFilepath(data);
	// 書き込む行データを作成
	let log = getLogLineFromData(data);
	// 行データを書き込む
	let dir = path.dirname(filepath);
	if (!fs.existsSync(dir)) fs.mkdirSync(dir);
	fs.writeFileSync(filepath, log, {flag:"as"});
}
/**
 * GSPデータから書き込むログファイルパスを取得する
 *
 * @param data String GPSデータ
 * @return String
 */
function getFilepath(data)
{
	// 日付のファイルを作成して返す
	let utc = getUtcFromData(data);
	let date = moment.utc(utc).tz("Asia/Tokyo").format("YYYYMMDD");
	let filename = date + ".log";
	let dir = './log';
	let filepath = dir + '/' + filename;
	return filepath;
}
/**
 * GPSデータからUTC日時を作成する
 *
 * @param data String GPSデータ
 * @return String
 */
function getUtcFromData(data)
{
	// $GPRMC,144733.00,A,3519.49685,N,13926.21062,E,0.072,,180521,,,A*7C
	let ary = data.split(',');
	let hour = ary[1].substr(0, 2);
	let min = ary[1].substr(2, 2);
	let sec = ary[1].substr(4, 2);
	let year = parseInt(ary[9].substr(4, 2)) + 2000;
	let month = ary[9].substr(2, 2);
	let day = ary[9].substr(0, 2);
	let utc = year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec;
	return utc;
}
/**
 * ログ行をGPSデータから作成する
 *
 * @param data String GPSデータ
 * @return String
 */
function getLogLineFromData(data)
{
	let utc = getUtcFromData(data);
	let date = moment.utc(utc).tz("Asia/Tokyo").format();
	let log = date + ' - ' + data + "\n";
	return log;
}
```
```
$ node ./gps.js
```
でシリアルポートから受信を開始し、`./log/YYYYMMDD.log` に受信内容が書き込まれます。 
こんな感じ

```YYYYMMDD.log
2021-05-20T00:00:00+09:00 - $GPRMC,150000.00,A,3519.50282,N,13926.21234,E,0.321,,190521,,,A*70
2021-05-20T00:00:01+09:00 - $GPRMC,150001.00,A,3519.50280,N,13926.21236,E,0.185,,190521,,,A*7D
2021-05-20T00:00:02+09:00 - $GPRMC,150002.00,A,3519.50248,N,13926.21231,E,0.405,,190521,,,A*70
2021-05-20T00:00:03+09:00 - $GPRMC,150003.00,A,3519.50233,N,13926.21229,E,0.366,,190521,,,A*76
2021-05-20T00:00:04+09:00 - $GPRMC,150004.00,A,3519.50223,N,13926.21227,E,0.439,,190521,,,A*73
2021-05-20T00:00:05+09:00 - $GPRMC,150005.00,A,3519.50219,N,13926.21227,E,0.095,,190521,,,A*79
2021-05-20T00:00:06+09:00 - $GPRMC,150006.00,A,3519.50216,N,13926.21227,E,0.080,,190521,,,A*71
2021-05-20T00:00:07+09:00 - $GPRMC,150007.00,A,3519.50182,N,13926.21233,E,0.449,,190521,,,A*7A
2021-05-20T00:00:08+09:00 - $GPRMC,150008.00,A,3519.50161,N,13926.21229,E,0.453,,190521,,,A*78
2021-05-20T00:00:09+09:00 - $GPRMC,150009.00,A,3519.50166,N,13926.21224,E,0.125,,190521,,,A*77
2021-05-20T00:00:10+09:00 - $GPRMC,150010.00,A,3519.50176,N,13926.21221,E,0.375,,190521,,,A*7C
2021-05-20T00:00:11+09:00 - $GPRMC,150011.00,A,3519.50165,N,13926.21220,E,0.166,,190521,,,A*7E
2021-05-20T00:00:12+09:00 - $GPRMC,150012.00,A,3519.50145,N,13926.21216,E,0.430,,190521,,,A*7C
```

## 地図に表示する
リアルタイムに地図に取得した位置を表示させるために、ブラウザからWebsocketsでデータをやりとりします。
Websocketsサーバとしては、いつもIoTで利用している軽量Pub/Sub 
mosquittoを利用します。

<img src="https://mosquitto.org/images/mosquitto-text-side-28.png" width="200px">
https://mosquitto.org/

Websocketsで接続して処理するjsライブラリはMQTT.jsを利用します。
<img src="https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png" width="200px">
https://github.com/mqttjs/MQTT.js/

GoogleMapを含む表示画面は、Webscocketsできればどこにあっても構いません。
今回、RaspberryPi4にnodejs ExpressでWebサーバを立てており、そこに表示ページを設置しています。

ページの実装内容は次の通り

```index.js
<!DOCTYPE html>
<html>
<head>
<title>GPS Logger</title>
<meta charset="utf-8">
<script src="/javascripts/mqtt.min.js"></script>
<link rel='stylesheet' href='/stylesheets/style.css' />
<script>

// Map
let map;
function initMap() {
	map = new google.maps.Map(document.getElementById('map'), {
    center: { lat: -34.397, lng: 150.644 },
    zoom: 20,
	});
	marker = new google.maps.Marker;
	marker.setMap(map);
	polyline = new google.maps.Polyline({
		path: [],
		strokeColor: "#728EF0",
		strokeOpacity: 0.8,
		strokeWeight: 3,
		geodesic: false
	});
}
let marker;
let polyline;

// MQTT
let mqttClient = mqtt.connect('ws://raspberrypi4.local:9090');
mqttClient.on('connect', () => {
	console.log('mqtt connected.');
	mqttClient.subscribe('gps');
});
mqttClient.on('message', (topic, message) => {
	readData(message.toString());
});

/**
 * GPSデータの読み込み
 *
 * @param json data {lat:val, lng:val}
 * @return void
 */
function readData(data)
{
	console.log(data);
	if (data == null) return ;
	// 緯度経度をMapの中心に配置
	let latlng = getLatLngFromData(data);
	if (latlng.lat != null && latlng.lng != null) {
		map.setCenter(latlng);
		// マーカーを設置
		marker.setPosition(latlng);
		// 軌跡を描く
		drawLine(latlng);
	}
}
/**
 * GPSデータから10進緯度経度を取得する
 *
 * @param strin data GPSデータ
 * @return json {lat:val, lng:val}
 */
function getLatLngFromData(data)
{
	let ary = data.split(',');
	latlng = {lat: null, lng: null};
	if (ary[3] && ary[5]) {
		// 経度 latitude
		let lats = [];
		let lat = String(ary[3]);
		lats[0] = parseInt(lat.substr(0, 2)); // 度
		lats[1] = parseFloat(lat.substr(2, lat.length-2))/60;
		lat = lats[0] + lats[1];

		// 緯度 longitude
		let lng = String(ary[5]);
		let lngs = [];
		lngs[0] = parseInt(lng.substr(0, 3)); // 度
		lngs[1] = parseFloat(lng.substr(3, lng.length-3))/60;
		lng = lngs[0] + lngs[1];

		latlng = {lat:lat, lng:lng};
		console.log(latlng);
	}
	return latlng;
}
/**
 * 軌跡を描く
 *
 * @param json latlng {lat:val, lng:val}
 * @return void
 */
function drawLine(latlng)
{
	let path = polyline.getPath();
	path.push(new google.maps.LatLng(latlng));
	polyline.setPath(path);
	polyline.setMap(map);
}

</script>
<style type="text/css">
html,
body {
	height: 100%;
	margin: 0;
	padding: 0;
}
#map { height: 100%; }

</style>
</head>

<body>
	<div id="map"></div>

  <script
    src="https://maps.googleapis.com/maps/api/js?key=<YOUR_KEY>&callback=initMap&libraries=&v=weekly"
    async
  ></script>
</body>
</html>
```

GPSレシーバーを受信できる家の窓際においておきます。
そしてWebブラウザで開くとこんな感じ。
10m範囲くらいでデータが動いています。
![llkk9-48ru0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4632/a02ca09a-c765-dedc-2eb5-99ad8d7be267.gif)

## まとめ
と、いった感じでGPSのNMEAフォーマットの緯度経度を含むGPRMCデータを使用して、リアルタイムでの地図表示を行いました。
ちょっとハマったのが、NMEAの60進の度数を10進に変換する時に、どこまでが度なのかわからなかった。
その際こちらが参考になりました。
[0183の座標の桁数は可変だった件](https://mitosuya.net/nmea-coordinate#NMEAGoogle_MAP)

## 次の目標
保存しているデータを、カレンダーから期間を指定してファイル読み込んで地図に描きたい。

以上でーす。