本プロジェクトは、2024年 SPRESENSE™ 活用コンテストのために作成されました。
はじめに
SpreM5GPSense(すーぱーえむふぁいぶじーぴーせんす)は、SPRESENSEのGPSデータをM5Stackのディスプレイ上に表示するGPS viewerおよびSDカードにGPS情報を記録する loggerです。
登場人物
今回、利用したのは、以下のような技術/デバイスです。
SPRESENSE
SPRESENSEは、Sonyの開発した高性能のマイコンボードです。
メインボード単体で、GPS情報が取得できたり、高性能の音の入出力、AI機能など色々なことができるようになっています。
マルチコアも利用可能で、全部で6つのコアを利用して、高い性能が求められる構成を取ることが可能です。
今回は、GPSによる情報をSPRESENSEから取得し、シリアル2経由でM5Stackにデータを送ります。
M5Stack
M5Stackは、オールインワンの使いやすいマイコンです。
今回利用したのはM5Stack Core2で、GPS情報を画面上に表示するために利用しました。
今回は、SPRESENSEと接続するために、PlusモジュールやGrove-pinケーブルなどを利用していますが、M5StackのMBUSからシリアル2の信号が出ているため、これらは無くても動作させることが可能です。
MBUSのシリアル2とSPRESENSEのシリアル2を繋いで下さい。
システム構成
ここでは、システムについて説明していきます。
今回のシステムは、できるだけシンプルにGPS viewer/loggerを構成することを目標にしています。
部品
システムで利用した部品は、以下の通りです。
名称 | SKU | 用途 |
---|---|---|
SPRESENSEメインボード | CXD5602PWBMAIN1 | GPSデータ取得 |
M5Stack Core2 | M5STACK-K010-V11 | SPRESENSEからのデータを表示 |
M5Stack Module Plus | M5STACK-PLUSEM | SPRESENSEとCore2の接続 |
GROVE - 4ピン - ジャンパオスケーブル | SEEED-110990210 | SPRESENSEとM5Stackの接続 |
micro SDカード | - | ロガー動作させる時のデータ記録用 |
モバイルバッテリー | - | 電源供給用:低電流対応版が必要 |
設計図
シリアル2はM5Stackのシリアル2と接続されており、データのやり取りを行います。
ソースコード
ソースコードは全て、github https://github.com/610t/SpreM5GPSense/ で公開しています。
SPRESENSソースコード
SPRESENSEでは、GPSデータを取得し、その値をシリアル2経由で出力するようになっています。
GPSデータの取得に関しては、スケッチ例のgnss.ino
を参考にして作成しました。
SPRESENSE.ino
#include <GNSS.h>
#define STRING_BUFFER_SIZE 128
#define RESTART_CYCLE (60 * 5) // Every 5min
static SpGnss Gnss;
void setup() {
int result;
Serial.begin(115200);
Serial2.begin(115200); // Connect to M5Stack via Serial2.
ledOn(PIN_LED0); // Turn on LED0 to indicate initialize
Gnss.setDebugMode(PrintInfo); // Set Debug mode to Info
if (Gnss.begin() != 0) {
Serial.println("Gnss begin error!!");
ledOn(PIN_LED1);
exit(0);
}
// Select all GPS mode.
Gnss.select(GPS);
Gnss.select(GLONASS);
Gnss.select(QZ_L1CA);
// Start positioning
if (Gnss.start(COLD_START) != 0) {
Serial.println("Gnss start error!!");
ledOn(PIN_LED2);
exit(0);
}
ledOff(PIN_LED0); /// Turn off LED0 :Setup done.
}
// Print received data Serial for debug and Serial2 for M5Stack
static void print_with_debug(char *strbuf) {
Serial.print(strbuf);
Serial2.print(strbuf);
}
static void print_pos(SpNavData *pNavData) {
char StringBuffer[STRING_BUFFER_SIZE];
// print date & time
snprintf(StringBuffer, STRING_BUFFER_SIZE, "Date:%04d/%02d/%02d\n", pNavData->
time.year, pNavData->time.month, pNavData->time.day);
print_with_debug(StringBuffer);
snprintf(StringBuffer, STRING_BUFFER_SIZE, "Time:%02d%02d%02d.%02d\n", pNavDat
a->time.hour, pNavData->time.minute, pNavData->time.sec, int(pNavData->time.usec
/ 10000));
print_with_debug(StringBuffer);
// print satellites count
snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSat:%2d\n", pNavData->numSatellites);
print_with_debug(StringBuffer);
snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSatCalc:%2d\n", pNavData->numSatellitesCalcPos);
print_with_debug(StringBuffer);
// HDOP
snprintf(StringBuffer, STRING_BUFFER_SIZE, "HDOP:%.1f\n", pNavData->hdop);
print_with_debug(StringBuffer);
// altitude
snprintf(StringBuffer, STRING_BUFFER_SIZE, "alt:%.1f\n", pNavData->altitude);
print_with_debug(StringBuffer);
// print position data
if (pNavData->posFixMode == FixInvalid) {
print_with_debug("Fix:No-Fix\n");
} else {
print_with_debug("Fix:Fix\n");
}
if (pNavData->posDataExist == 0) {
Serial.print("Post:No Position\n");
} else {
snprintf(StringBuffer, STRING_BUFFER_SIZE, "Lat:%f\nLon:%f\n", pNavData->latitude, pNavData->longitude);
print_with_debug(StringBuffer);
}
}
static void print_condition(SpNavData *pNavData) {
char StringBuffer[STRING_BUFFER_SIZE];
unsigned long cnt;
// Print satellite count.
snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSat:%2d\n", pNavData->numSatellites);
print_with_debug(StringBuffer);
for (cnt = 0; cnt < pNavData->numSatellites; cnt++) {
const char *pType = "GPS";
SpSatelliteType sattype = pNavData->getSatelliteType(cnt);
// Get print conditions.
unsigned long Id = pNavData->getSatelliteId(cnt);
unsigned long Elv = pNavData->getSatelliteElevation(cnt);
unsigned long Azm = pNavData->getSatelliteAzimuth(cnt);
float sigLevel = pNavData->getSatelliteSignalLevel(cnt);
// Print satellite condition.
snprintf(StringBuffer, STRING_BUFFER_SIZE, "[%2ld] Type:%s, Id:%2ld, Elv:%2ld, Azm:%3ld, CN0:", cnt, pType, Id, Elv, Azm);
Serial.print(StringBuffer);
Serial.println(sigLevel, 6);
}
}
void loop() {
static int LoopCount = 0;
static int LastPrintMin = 0;
// Check update.
if (Gnss.waitUpdate(-1)) {
// Get NaviData.
SpNavData NavData;
Gnss.getNavData(&NavData);
// Print satellite information to Serial every minute.
if (NavData.time.minute != LastPrintMin) {
print_condition(&NavData);
LastPrintMin = NavData.time.minute;
}
// Send position information via Serial2
print_pos(&NavData);
} else {
// Not update.
Serial.println("data not update");
}
// Check loop count.
LoopCount++;
if (LoopCount >= RESTART_CYCLE) {
int error_flag = 0;
// Restart GNSS.
if (Gnss.stop() != 0) {
Serial.println("Gnss stop error!!");
error_flag = 1;
} else if (Gnss.end() != 0) {
Serial.println("Gnss end error!!");
error_flag = 1;
} else {
Serial.println("Gnss stop OK.");
}
if (Gnss.begin() != 0) {
Serial.println("Gnss begin error!!");
error_flag = 1;
} else if (Gnss.start(HOT_START) != 0) {
Serial.println("Gnss start error!!");
error_flag = 1;
} else {
Serial.println("Gnss restart OK.");
}
// If error on LED3 and halt.
if (error_flag == 1) {
ledOn(PIN_LED3);
exit(0);
}
LoopCount = 0;
}
}
M5Stackソースコード
M5Stackのソースコードは、以下の通りです。
viewerのみのコード
viewerだけのコードは、以下のようにとてもシンプルになります。
M5Unifiedを利用することで、M5Stackファミリーの様々な機種に対応できるだけではなく、シリアル2で受け取ったデータをディスプレイに簡単に表示できるようになっています。
M5Stack_viewer.ino
#include <M5Unified.h>
void setup() {
Serial.begin(115200); // for debug output
Serial2.begin(115200); // get data from SPRESENSE
M5.begin();
M5.setLogDisplayIndex(0);
M5.Display.setTextScroll(true);
M5.Lcd.setTextSize(2);
}
void loop() {
char buf[1024] = { 0 };
int av = Serial2.available();
if (av > 0) {
Serial2.readBytes(buf, av);
M5.Log.printf("%s\n", buf);
}
}
loggerを含むコード
以下のコードは、SDへのログを取得できるようにしたものです。
ボタンAでログが開始され、ボタンBでログが停止されます。
ログのフォーマットは、NMEA形式にしているつもりなのですが、現状ではうまく変換アプリケーションで変換できないものになっています。
原因がわかり次第直したいと思っています。
M5Stack.ino
/*
Function CoordinateToString() and the logic of caluclating GPGGA checksum is based on gnss_nmea.cpp.
See license below.
*/
/*
* gnss_nmea.cpp - NMEA's GGA sentence
* Copyright 2017 Sony Semiconductor Solutions Corporation
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <SD.h>
#include <M5Unified.h>
bool logger_mode = false;
bool sd_exist = false;
bool data_update = false; // Is data updated?
const static char gps_log_filename[] = "/GPS/gps_log.nmea";
#define CORIDNATE_TYPE_LATITUDE 0 /**< Coordinate type latitude */
#define CORIDNATE_TYPE_LONGITUDE 1 /**< Coordinate type longitude */
static void CoordinateToString(char *pBuffer, int length, double Coordinate,
unsigned int cordinate_type) {
double tmp;
int Degree;
int Minute;
int Minute2;
char direction;
unsigned char fixeddig;
const static struct {
unsigned char fixeddigit;
char dir[2];
} CordInfo[] = {
{ .fixeddigit = 2, .dir = { 'N', 'S' } },
{ .fixeddigit = 3, .dir = { 'E', 'W' } },
};
if (cordinate_type > CORIDNATE_TYPE_LONGITUDE) {
snprintf(pBuffer, length, ",,");
return;
}
if (Coordinate >= 0.0) {
tmp = Coordinate;
direction = CordInfo[cordinate_type].dir[0];
} else {
tmp = -Coordinate;
direction = CordInfo[cordinate_type].dir[1];
}
fixeddig = CordInfo[cordinate_type].fixeddigit;
Degree = (int)tmp;
tmp = (tmp - (double)Degree) * 60 + 0.00005;
Minute = (int)tmp;
tmp = (tmp - (double)Minute) * 10000;
Minute2 = (int)tmp;
snprintf(pBuffer, length, "%0*d%02d.%07d,%c", fixeddig, Degree, Minute, Minute2, direction);
}
void setup() {
Serial.begin(115200); // for debug output
Serial2.begin(115200); // get data from SPRESENSE
M5.begin();
M5.setLogDisplayIndex(0);
M5.Display.setTextScroll(true);
M5.Lcd.setTextSize(2);
M5.Log.printf("SpreM5GPSense start!!\n");
#define MAX_SD_WAIT 5
// Initialize SD
// If SD can't find MAX_SD_WAIT, run viewer only mode.
int i;
for (i = 0; i < MAX_SD_WAIT; i++) {
if (SD.begin(GPIO_NUM_4, SPI, 15000000)) {
break;
}
M5.Log.printf("SD Wait...\n");
delay(500);
}
if (i != MAX_SD_WAIT) {
M5.Log.printf("* Found SD\n");
sd_exist = true;
if (!SD.exists("/GPS")) {
SD.mkdir("/GPS");
}
} else {
M5.Log.printf("* Run viewer only mode\n");
Serial2.read(); // Discard all incoming data.
sd_exist = false;
}
}
String getStrValue(String data, String pattern) {
data.replace(pattern.c_str(), "");
return (data);
}
float getFloatValue(String data, String pattern) {
data.replace(pattern.c_str(), "");
return (data.toFloat());
}
int getIntValue(String data, String pattern) {
data.replace(pattern.c_str(), "");
return (data.toInt());
}
void loop() {
M5.update();
// logger mode on/off
if (M5.BtnA.wasClicked() && logger_mode == false) {
logger_mode = true;
M5.Power.setLed(255);
M5.Log.printf("* Logger mode on.\n");
} else if (M5.BtnB.wasClicked() && logger_mode == true) {
logger_mode = false;
M5.Power.setLed(0);
M5.Log.printf("* Logger mode off.\n");
}
// GPS data
String date, time;
int numSat, numSatCalc;
float lat, lon;
float hdop;
float alt;
bool fix_state;
int av = Serial2.available();
if (av) {
M5.Lcd.clear();
M5.Lcd.setCursor(0, 0);
}
while (av > 0) {
String line = Serial2.readStringUntil('\n');
av = Serial2.available();
M5.Log.printf("%s\n", line.c_str());
// Convert to NMEA GGA format
if (line.startsWith("Date:")) {
date = getStrValue(line, "Date:");
} else if (line.startsWith("Time:")) {
time = getStrValue(line, "Time:");
} else if (line.startsWith("numSat:")) {
numSat = getIntValue(line, "numSat:");
} else if (line.startsWith("numSatCalc:")) {
numSatCalc = getIntValue(line, "numSatCalc:");
} else if (line.startsWith("Lat:")) {
lat = getFloatValue(line, "Lat:");
} else if (line.startsWith("Lon:")) {
lon = getFloatValue(line, "Lon:");
} else if (line.startsWith("alt:")) {
alt = getFloatValue(line, "alt:");
} else if (line.startsWith("HDOP:")) {
hdop = getFloatValue(line, "HDOP:");
} else if (line.startsWith("Fix:")) {
String fix = getStrValue(line, "Fix:");
if (fix.startsWith("Fix")) {
fix_state = true;
} else {
fix_state = false;
}
}
data_update = true;
}
// Log to SD
if (data_update) {
data_update = false;
if (logger_mode && sd_exist) {
// Write out NMEA GGA data to SD
if (fix_state) {
#define STRING_BUFFER_SIZE 1024
char StringBuffer[STRING_BUFFER_SIZE];
char latStr[STRING_BUFFER_SIZE];
char lonStr[STRING_BUFFER_SIZE];
CoordinateToString(latStr, STRING_BUFFER_SIZE, lat, CORIDNATE_TYPE_LATITUDE);
CoordinateToString(lonStr, STRING_BUFFER_SIZE, lon, CORIDNATE_TYPE_LONGITUDE);
snprintf(StringBuffer, STRING_BUFFER_SIZE, "$GPGGA,%s,%s,%s,1,%02d,%.1f,%.1f,M,34.05,M,1.0,512*", time.c_str(), latStr, lonStr, numSatCalc, hdop, alt);
String gga = StringBuffer;
// Calculate checksum: based on gnss_nmea.cpp.
unsigned short CheckSum = 0;
{
int cnt;
const char *pStrDest = gga.c_str();
/* Calculate checksum as xor of characters. */
for (cnt = 1; pStrDest[cnt] != 0x00; cnt++) {
CheckSum = CheckSum ^ pStrDest[cnt];
}
}
snprintf(StringBuffer, STRING_BUFFER_SIZE, "%02X\r\n", CheckSum);
gga += StringBuffer;
M5.Log.printf(gga.c_str());
// Output to SD
File GPSFile = SD.open(gps_log_filename, FILE_APPEND);
GPSFile.printf(gga.c_str());
GPSFile.close();
}
}
}
}
動作の様子
実際の動作の様子を以下の動画に示します。
おわりに
SPRESENSEとM5Stackを使って、GPSのviewerとloggerを作ってみました。
もちろん、SPRESENSEに拡張ボードを追加してSDを利用し、ディスプレイとしてILI9341などを追加することでGPS viewer/loggerは作れます。
しかし、M5StackのSDとディスプレイを流用することで、同じようなシステムを簡単に作ることができました。
このようなシステム構成にすると、他にもM5StackのWiFi/BLEなどの機能も利用可能になるため、作品の可能性が広がると思います。
Enjoy your SPRESENSE life with M5Stack!!
このプロジェクトについて
このプロジェクトの開発の一部は、大晦日ハッカソン2024で行いました。
投稿者の人気記事
-
610t
さんが
2024/12/31
に
編集
をしました。
(メッセージ: 初版)
-
610t
さんが
2025/01/01
に
編集
をしました。
ログインしてコメントを投稿する