概要
車で旅に出る時に軌跡を残しておきたいなと思って、スマホのアプリで軌跡を残していましたが、ちょっとGPSのデータを取得してみたい。実際のGPSのデータってどうなってんの?が今回の作業のきっかけ。
Google MyMapで、GPSロガーのデータをインポートすればKMLに変換してくれて表示してくれるけど、ちょっと自分でやってみよう!
メインホスト
今、手元にRaspberryPi4Bが3台あります。
メインホストはこれを選択。
GPSの購入
RasberryPi HAT形式のGPSを考えていましたが、遊ぶにはちょっとお高めなので、USB接続のGPSレシーバーを購入。
Amazon GPSレシーバー GPS受信機 DOCTORADIO GR-7BN アンテナ内蔵 チップ一体型 USB出力 測位 時刻較正用
構成
- RaspberryPi4にUSBでGPSレシーバーと繋ぐ
- USBを介してシリアルでGPSレシーバからNMEAフォーマットのデータを受信する
- シリアルから1行づつデータを、nodejsで組んだプログラムが読み込む
- 読み込んだデータをLocalStorageに書き込む(ファイル)
- 同時にmosquittoのMQTT Pub/SubにPublishする
- WebBrowserで、MQTT Pub/SubをWebsocketsで受信するためにSubscribeしておく
- WebBrowserで受信したデータをパースして、10進の緯度経度に変換して、GoogleMapにプロットする
- 位置データをPolylineで結んで軌跡を表示させる
GPSデータの受信と保存
GPSのデータはシリアルポートから受信する。
GPSレシーバーをUSBのここに刺すと、/dev/ttyACM0
で読むことができる。
取得対象はGPRMCのみとする。
下記のreadDataメソッドのdata変数をconsole.logで出力すれば内容を確認できます。
GPSデータのNMEAフォーマットは?に関しては、以下を参照してください。
GPSのNMEAフォーマット
GPSデータではGPRMCのみ対象としています。
取得した内容をファイルに書き込んでいます。
// 日付モジュール
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
に受信内容が書き込まれます。
こんな感じ
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を利用します。
Websocketsで接続して処理するjsライブラリはMQTT.jsを利用します。
https://github.com/mqttjs/MQTT.js/
GoogleMapを含む表示画面は、Webscocketsできればどこにあっても構いません。
今回、RaspberryPi4にnodejs ExpressでWebサーバを立てており、そこに表示ページを設置しています。
ページの実装内容は次の通り
<!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範囲くらいでデータが動いています。
まとめ
と、いった感じでGPSのNMEAフォーマットの緯度経度を含むGPRMCデータを使用して、リアルタイムでの地図表示を行いました。
ちょっとハマったのが、NMEAの60進の度数を10進に変換する時に、どこまでが度なのかわからなかった。
その際こちらが参考になりました。
0183の座標の桁数は可変だった件
次の目標
保存しているデータを、カレンダーから期間を指定してファイル読み込んで地図に描きたい。
以上でーす。