LoginSignup
5

More than 1 year has passed since last update.

RaspberryPi で、GPS Logger & Tracker の製作

Posted at

概要

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

メインホスト

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

GPSの購入

RasberryPi HAT形式のGPSを考えていましたが、遊ぶにはちょっとお高めなので、USB接続のGPSレシーバーを購入。
Amazon GPSレシーバー GPS受信機 DOCTORADIO GR-7BN アンテナ内蔵 チップ一体型 USB出力 測位 時刻較正用

構成

  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で結んで軌跡を表示させる

GPSデータの受信と保存

GPSのデータはシリアルポートから受信する。
GPSレシーバーをUSBのここに刺すと、/dev/ttyACM0 で読むことができる。
image.png

取得対象はGPRMCのみとする。
下記のreadDataメソッドのdata変数をconsole.logで出力すれば内容を確認できます。
GPSデータのNMEAフォーマットは?に関しては、以下を参照してください。
GPSのNMEAフォーマット

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を利用します。


https://mosquitto.org/

Websocketsで接続して処理するjsライブラリはMQTT.jsを利用します。

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

まとめ

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

次の目標

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

以上でーす。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5