u-blox GNSSモジュール+ESP32でみちびき（QZSS）の「災危通報」情報を受信する

# 目的
u-blox GNSSモジュールとESP32（もしくはArduino）を接続し、UBXプロトコルのメッセージを解読して、みちびきが送信する災害・危機管理通報サービス「災危通報」を受信します。

SPRESENSEで災危通報を受信する記事は多くありますが、u-blox社のモジュールの方が取得可能な情報や設定できる項目という面で自由度が高く、u-bloxモジュールで災危通報を受信したいこともあると思うので、ESP32で災危通報を受信する方法を紹介します。
また、この記事では汎用性をもたせるため、u-bloxモジュールが送信するUBXメッセージを1バイトずつ処理する方法を紹介していますので、災危通報情報以外のサブフレームメッセージや、ほかのUBXメッセージの取り扱い [^1] も可能です。

[^1]: モジュールにもよりますが、内臓スペアナの値を取ったり、ジャミング検知結果などを取得できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/0ce3d2cb-0310-9750-832c-33aaae8a74c1.png)

# 「災危通報」情報とは
QZSS(みちびき）では、地震や津波、気象警報など災害・危機管理通報（災危通報）情報を配信するサービスが提供されており、QZSSを受信できるエリアであれば誰でも災危通報情報を取得できます。
2023/10時点では防災気象情報のみの配信となっていますが、ミサイル発射情報・避難指示情報などの配信についても検討されているようです[^100]。

[^100]: https://qzss.go.jp/overview/services/sv08_dc-report.html

# 環境

## 用意した機材
* Seeed Studio XIAO ESP32C3
* u-blox NEO-M9Nが搭載されたGNSSモジュール
    * GNSS 7 CLICK
* アクティブGPS/GNSSアンテナ
    * https://www.switch-science.com/products/8631

GNSSモジュールについては、
* I2CやUARTを通じてESP32に接続できること
* QZSS（みちびき）に対応していること
* L1S信号を受信でき、SFRBXメッセージをI2CやUARTに送信できること
が必須条件になります。
任意の条件としては、
* USBポートが存在すること
    * 初期設定がGUIでできるため
* アクティブGPSアンテナの接続に対応していること
    * 室内でも災危通報が受信しやすくなり、デバッグが楽になるため
* です。

対応モジュールとして、公式[^2]にはu-blox `MAX-M10S`と`ZED-F9P`が挙げられてますが、より安価なNEO-M9Nでも問題なく受信できました。

[^2]: https://qzss.go.jp/usage/products/dc-report.html

## 接続
ESP32とGNSSモジュールをUARTで接続しました。

# ステップ1: u-bloxモジュールの設定
初期状態では、u-bloxモジュールはサブフレームを出力しません。
そのため、まずGNSSモジュールとPCをUSBで接続し、公式評価ソフトウェアであるu-centerを使って、モジュールの設定を変更していきます。
評価ソフトウェアは以下のページからダウンロード可能です。
https://www.u-blox.com/en/product/u-center
Windows環境がない場合やUSB接続できない場合は、UBXメッセージを直接モジュールに送信して設定変更する方法がありますが、この記事では割愛します。

u-centerを開いた後、以下のように設定を行ってください。

1. メニュー->Receiver->ConnectionからモジュールのCOMポートを選択
1. Ctrl+F9を押下してConfiguration Viewを開く
1. MSG(Messages)->Message:02-13 RXM-SFRBXを選び、USB・UART（I2C）にチェックを入れる
    * ESP32の接続先にSFRBXメッセージが流れるようにしてください
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/82523340-f3a2-c4b0-285e-7cf517bf44c3.png)

1. 画面の下部にあるSendを押下
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/8ef90f87-2322-37b6-7659-58457baf2cb5.png)
1. CFG(Configuration)で、以下の図のように項目を選び、画面の下部にあるSendを押下。これにより、モジュールの電源を入れなおしても設定が保持されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/bfa791b9-2f78-fa02-9c84-50d90855efc8.png)

1. F9を押下し、Messages Viewを開く
1. UBX->RXM->SFRBXを開き、メッセージが表に出力されていることを確認する
    * QZSS L1Sが出力されていることを確認してください。QZSSが出力されない場合、アンテナの位置／設定を再確認してみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/bc023f5a-9204-2b24-e078-87a1e00191b2.png) 

# ステップ2: NMEAメッセージとUBXメッセージを分ける

ここからコードを書いていきます。
u-bloxモジュールは、位置情報や日時情報をNMEAメッセージとして送りますが、サブフレームはUBXメッセージ形式で送信されます。前者は人間が読める形式ですが、後者はバイナリデータです。
NMEAメッセージは`$G`から、UBXメッセージは`0xb5 0x62`で始まるため、ひとまず以下のようにして処理を分けます。

:::note warn
以降に示すサンプルコードは、UARTから想定外のデータが到着したときのことを一切考えていないです。
:::

```cpp
#include <Arduino.h>

HardwareSerial gps_serial(1);

byte gpsdata_tmp[1024];
int gpsdata_tmp_pos = 0;
void setup()
{
  Serial.begin(115200);
  gps_serial.begin(115200, SERIAL_8N1, 20, 21);
}

void loop()
{
  if (gps_serial.available())
  {
    gpsdata_tmp[gpsdata_tmp_pos] = gps_serial.read(); //1バイト読み込み
    if (gpsdata_tmp_pos == 0 && gpsdata_tmp[0] != '$' && gpsdata_tmp[0] != 0xB5)
    {
      gpsdata_tmp_pos = 0;
    }
    else if (gpsdata_tmp_pos == 1 && gpsdata_tmp[0] == '$' && gpsdata_tmp[1] == 'G')
    {
      Serial.print("[NMEA0183]");
      String NMEA = "$G" + gps_serial.readStringUntil('\n');
      gpsdata_tmp_pos = 0;
    }
    else if (gpsdata_tmp_pos == 1 && gpsdata_tmp[0] == 0xB5 && gpsdata_tmp[1] == 0x62)
    {
      Serial.print("[UBX]");
      gpsdata_tmp_pos = 0;
    }
    else if (gpsdata_tmp_pos == 1)
    {
      gpsdata_tmp_pos = 0;
    }
    else
    {
      gpsdata_tmp_pos++;
    }
  }
}
```

# ステップ2: UBXメッセージのペイロードを取得する
UBXメッセージの仕様については、以下に詳しくまとめられています。
https://qiita.com/key/items/595132361f2935ae1bb7
UBXメッセージの詳細については以下の公式ドキュメントを参考にしました。
https://content.u-blox.com/sites/default/files/ZED-F9T-10B_InterfaceDescription_UBX-20033631.pdf
NEO-M9Nのドキュメントが見当たらなったので、ZED-F9T-10Bのものを参照しています。


まずは、UBXメッセージからCLASS, ID, PAYLOADを取得します。併せてチェックサムを計算します。
```cpp
...
else if (gpsdata_tmp_pos == 1 && gpsdata_tmp[0] == 0xB5 && gpsdata_tmp[1] == 0x62)
    {
      gpsdata_tmp_pos++;
      uint8_t ck_a = 0, ck_b = 0; //チェックサム計算用
      uint8_t ok_a = 0, ok_b = 0; //チェックサム計算用
      bool payload_flag = false; //Payloadが読み込み終わったフラグ
      bool valid_flag = false; //チェックサムが正しかった場合のフラグ
      uint16_t length = 0;
      while (true) //UBXパケットを読む
      {
        if (gps_serial.available())
        {
          gpsdata_tmp[gpsdata_tmp_pos] = gps_serial.read();
          if (gpsdata_tmp_pos >= 2 && !payload_flag) //Payloadの読み込みが終わるまでチェックサムの計算を行う
          {
            ck_a = (ck_a + gpsdata_tmp[gpsdata_tmp_pos]) & 0xFF;
            ck_b = (ck_b + ck_a) & 0xFF;
          }
          if (gpsdata_tmp_pos == 5) //Payload長を取得する
          {
            length = gpsdata_tmp[4];
            length += (gpsdata_tmp[5] << 8);
          }
          else if (gpsdata_tmp_pos == length + 5) //Payloadが読み込み終わったらフラグを入れる
          {
            payload_flag = true;
          }
          else if (gpsdata_tmp_pos == length + 7) //チェックサムの確認
          {
            ok_a = gpsdata_tmp[length + 6];
            ok_b = gpsdata_tmp[length + 7];
            if (ck_a == ok_a && ck_b == ok_b)
            {
              Serial.print("[checksum OK]");
              valid_flag = true; //フラグを入れPayloadを解析するようにする
            }
            else
            {
              Serial.print("[checksum NG]");
              gpsdata_tmp_pos = 0;
            }
            break;
          }
          gpsdata_tmp_pos++;
        }
      }
      if (valid_flag) //UBX Payloadの解析
      {
        gpsdata_tmp_pos = 0;
        //Payloadを解析するプログラム
      }
```

# ステップ3: ペイロードを解析する

公式ドキュメントによると、サブフレーム（SFBRX）データの場合、
UBXメッセージのCLASSが`0x02`、IDが`0x13`となります。
そのため、まずUBXメッセージがサブフレームかどうかを確認します。

次にSFBRXメッセージの送信元がQZSSであることを確認します。
そのためには、SFBRXのGNSS identifier（Payloadの0バイト目）を取得します。
GNSS identifierが5のとき、QZSSが送信元となります。
```cpp
...
      if (valid_flag) //UBX Payloadの解析
      {
        if (gpsdata_tmp[2] == 0x02 && gpsdata_tmp[3] == 0x13) //SFBRXメッセージか否か
        {
          Serial.print("[SFBRX]");
          if (gpsdata_tmp[6] == 0x05) //PayloadはUBXメッセージの6バイト目から開始
          {
            Serial.print("[QZSS]");
            //SFRBXのPayloadを解析するプログラム
          }
        }
...
```

# ステップ4: サブフレームデータを取得する
サブフレームデータはPayloadの8バイト目、つまりUBXメッセージの14バイト目から始まります。
サブフレームデータは1ワード単位で構成され、ワード数は可変です。
ワード数はPayloadの4バイト目に書かれているので取得しておきます。
サブフレームでは、1ワードが4バイトとなります。
```cpp
        if (gpsdata_tmp[2] == 0x02 && gpsdata_tmp[3] == 0x13)
        {
          Serial.print("[SFBRX]");
          if (gpsdata_tmp[6] == 0x05)
          {
            Serial.print("[QZSS]");
            uint8_t numWords = gpsdata_tmp[10]; //ワード数
            Serial.printf("[numWords=%d]", numWords);
            uint8_t dcr[numWords * 4]; //サブフレームデータの格納先
            for (int i = 0; i < numWords * 4; i += 4) //1ワードごとに処理
            {
              dcr[i + 0] = gpsdata_tmp[14 + i + 3];
              dcr[i + 1] = gpsdata_tmp[14 + i + 2];
              dcr[i + 2] = gpsdata_tmp[14 + i + 1];
              dcr[i + 3] = gpsdata_tmp[14 + i + 0];
              Serial.printf("%02x%02x%02x%02x ", dcr[i + 0], dcr[i + 1], dcr[i + 2], dcr[i + 3]);
            }
            Serial.println();
          }
        }
```

# ステップ5: サブフレームデータをデコードし、災危通報情報を得る

下記のライブラリを用います。
https://github.com/baggio63446333/QZQSM

:::note warn
2023/9現在の「災危通報情報」の仕様に基づいてサンプルコードを掲載しています。
:::

```cpp
#include <QZQSM.h>
...
        if (gpsdata_tmp[2] == 0x02 && gpsdata_tmp[3] == 0x13)
        {
...
            QZQSM report;
            report.Decode(dcr);
            Serial.println(report.GetReport());
...
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123247/f64016b1-96fd-026f-c29e-f17be6798af5.png)

ライブラリ内ではMT（MessageType）の確認を行っておらず、災危通報情報ではないサブフレームデータがデコードされることがあります。
以下の資料によると、
https://qzss.go.jp/en/technical/download/dc-report/users-manual_ver1.pdf?t=1626019216491
MTが`43`のとき、気象庁 防災気象情報になるため、MTを確認してからライブラリにサブフレームデータを渡します。
MTは、サブフレームデータの（0から数えて）8ビット目から6ビット分になります。
```cpp
...
            uint8_t MT = (dcr[1] & 0b11111100) >> 2;
            if (MT == 43)
            {
              QZQSM report;
              report.Decode(dcr);
              Serial.println(report.GetReport());
...
````
以上により、災危通報情報を取得することができます。

# 全体のコード

:::note warn
UARTから想定外のデータが到着したときのことを一切考えていないため、実際に使用される際は確保したバッファ領域以外のアクセスが行われないようにするなど、対策を行ってください。
:::


``` cpp
#include <Arduino.h>
#include <QZQSM.h>

HardwareSerial gps_serial(1);

byte gpsdata_tmp[1024];
int gpsdata_tmp_pos = 0;
void setup()
{
  Serial.begin(115200);
  gps_serial.begin(115200, SERIAL_8N1, 20, 21); //UARTのPIN番号は環境により異なる
}

void loop()
{
  if (gps_serial.available())
  {
    gpsdata_tmp[gpsdata_tmp_pos] = gps_serial.read();
    if (gpsdata_tmp_pos == 0 && gpsdata_tmp[0] != '$' && gpsdata_tmp[0] != 0xB5)
    {
      gpsdata_tmp_pos = 0;
    }
    else if (gpsdata_tmp_pos == 1 && gpsdata_tmp[0] == '$' && gpsdata_tmp[1] == 'G')
    {
      String NMEA = "$G" + gps_serial.readStringUntil('\n');
      gpsdata_tmp_pos = 0;
    }
    else if (gpsdata_tmp_pos == 1 && gpsdata_tmp[0] == 0xB5 && gpsdata_tmp[1] == 0x62)
    {
      Serial.print("[UBX]");
      gpsdata_tmp_pos++;
      uint8_t ck_a = 0, ck_b = 0;
      uint8_t ok_a = 0, ok_b = 0;
      bool payload_flag = false;
      bool valid_flag = false;
      uint16_t length = 0;
      while (true)
      {
        if (gps_serial.available())
        {
          gpsdata_tmp[gpsdata_tmp_pos] = gps_serial.read();
          if (gpsdata_tmp_pos >= 2 && !payload_flag)
          {
            ck_a = (ck_a + gpsdata_tmp[gpsdata_tmp_pos]) & 0xFF;
            ck_b = (ck_b + ck_a) & 0xFF;
          }
          if (gpsdata_tmp_pos == 5)
          {
            length = gpsdata_tmp[4];
            length += (gpsdata_tmp[5] << 8);
          }
          else if (gpsdata_tmp_pos == length + 5)
          {
            payload_flag = true;
          }
          else if (gpsdata_tmp_pos == length + 7)
          {
            ok_a = gpsdata_tmp[length + 6];
            ok_b = gpsdata_tmp[length + 7];
            if (ck_a == ok_a && ck_b == ok_b)
            {
              Serial.print("[checksum OK]");
              valid_flag = true;
            }
            else
            {
              Serial.print("[checksum NG]");
              gpsdata_tmp_pos = 0;
            }
            break;
          }
          gpsdata_tmp_pos++;
        }
      }
      if (valid_flag)
      {
        if (gpsdata_tmp[2] == 0x02 && gpsdata_tmp[3] == 0x13)
        {
          Serial.print("[SFBRX]");
          if (gpsdata_tmp[6] == 0x05)
          {
            Serial.print("[QZSS]");
            uint8_t numWords = gpsdata_tmp[10];
            Serial.printf("[numWords=%d]", numWords);
            uint8_t dcr[numWords * 4];
            for (int i = 0; i < numWords * 4; i += 4)
            {
              dcr[i + 0] = gpsdata_tmp[14 + i + 3];
              dcr[i + 1] = gpsdata_tmp[14 + i + 2];
              dcr[i + 2] = gpsdata_tmp[14 + i + 1];
              dcr[i + 3] = gpsdata_tmp[14 + i + 0];
              Serial.printf("%02x%02x%02x%02x ", dcr[i + 0], dcr[i + 1], dcr[i + 2], dcr[i + 3]);
            }
            Serial.println();
            // check MT
            uint8_t MT = (dcr[1] & 0b11111100) >> 2;
            if (MT == 43)
            {
              QZQSM report;
              report.Decode(dcr);
              Serial.println(report.GetReport());
            }
          }
          else
          {
            Serial.println();
          }
        }
        else
        {
          Serial.println();
        }
        gpsdata_tmp_pos = 0;
      }
    }
    else if (gpsdata_tmp_pos == 1)
    {
      gpsdata_tmp_pos = 0;
    }
    else
    {
      gpsdata_tmp_pos++;
    }
  }
}

```

これで、インターネットに依存せず、みちびきが見える範囲であればどこでも「災危通報」情報を受信できるようになりました。