古事連記帖

趣味のこと、技術的なこと、適当につらつら書きます。

Windows 10 IoT と GPS センサーを使って電子工作した

前回に引き続き位置情報関連ネタです。

位置情報共有サービス「Locapos (ろけぽす)」を使った Windows 10 クライアントアプリ「Mikaboshi (みかぼし)」をリリースしたことは前回も同様ご紹介したとおりです。
ayano.hateblo.jp

id:tmyt が作った Locapos に対応する何か新しいものを作ろうということで、自動車の盗難時などの位置追跡ができないかやってみることにしました。


記事が長いので折りたたんでおきます…


用意したもの

通信には SORACOM Air を使うことにしました。ツイートの写真では SORACOM スターターキットが映っていますが、Windows 10 IoT では AK-020 が使用できなかったため、いろいろあって PIX-MT100 に落ち着きました。

GPS モジュールと Raspberry Pi3 を繋げる

まず GPS モジュールの本体とピンをはんだ付けします。はんだ付けは大学の実習で 1 回やったきりだったので、めっちゃ手が震えましたがなんとかやりました…。
はんだ付けが終わったら Raspberry Pi3 と接続します。今回は GPIO ピンに接続します。ブレッドボードに GPS モジュールをぶっ挿し、Raspberry Pi3 の有線 LAN ポートと USB-A コネクターのある方を下にしたとき (ピンの列が本体の右側になるように向かって縦にしたとき) の

  • 右上から 5 番目の 10 番ピン (UART0_RXD) と、GPS モジュールの TXD ピンを接続します。
  • 右上の 1 番目の 2 番ピン (5v) と、GPS モジュールの 5V ピンを接続します。
  • 右上から 3 番目の 6 番ピン (GND) と、GPS モジュールの GND ピンを接続します。

GPS モジュールの RXD と 1PPS ピンは今回使用しませんので接続しませんでした。GPS モジュールは電源が入ると自動で GPS 衛星の捕捉が始まり、情報が常に送信されるようになるので、通常利用ではこれだけでいいはずです。USB シリアル変換キットと接続する場合や、モジュールに対して何かコマンドを送信する場合は別途接続するといいかと思います。

f:id:ChiiAyano:20170811223403j:plain

こんな感じでパッケージ組みました。

GPS モジュールが出力するデータについて

GPS モジュールからは NMEA フォーマットの文字情報が、特に設定していなければ 1 秒間隔で流れてきます。
NMEA フォーマットは $ で始まり \r\n で終わる 1 行のデータで送られてきますので、これを利用します。
受け取れるフォーマットの詳細は以下の記事をご参照下さい。

GPSのNMEAフォーマット

プログラムを組む

プログラムを組まないことには何も始まらないので、ここまでできたらプログラムを組みます。
前述の通り、今回は Raspberry Pi3 に Windows 10 IoT Core を入れました。やりたいこと自体は Linux 系でも同じようにできますが、C# で作りたかったというのと、ネタ作りのために Windows 10 IoT にしてみました。これがちょっと罠だったんですが…。


この時点で、GPS モジュールとはシリアル通信でやりとりができるようになっています。従って、プログラムではシリアル通信で受け取ればいいわけです。
UWP でシリアルポートを使用する場合は、Package.appmanifest を直接開き、Capabilities タグ内に以下の内容を記述します。

<DeviceCapability Name="serialcommunication">
  <Device Id="any">
    <Function Type="name:serialPort" />
  </Device>
</DeviceCapability>


次にシリアルポートの初期化をします。デバイス一覧を取得し、最初に得られるデバイス情報で受け取れたのでそのまま使います。

public async Task InitializeAsync()
{
    // シリアルポートの初期化
    var selector = SerialDevice.GetDeviceSelector();
    var devices = await DeviceInformation.FindAllAsync(selector);

    this.device = devices.FirstOrDefault(); // device は DeviceInformation
}


シリアルポートを開いてデータ受け取りを待ちます。GPS モジュールの取説にあるとおり、特に設定を変更していなければボー レートは 9600 bps にします。
受け取り口になるストリームは (自分が扱いやすくしたいので) .NET Framework の System.IO.Stream に変換しました。Windows Runtime のストリーム型で扱える場合はそっちがいいかもしれない?


バッファーサイズは 0 にしています。1024 など設定した場合、バッファーに溜まるまでデータを返さない仕組み上中途半端にデータが途切れる場合があったため、即座にデータを吐き出すように 0 を指定しました。

public async Task OpenPort()
{
    if (this.device == null) return;

    this.serialPort = await SerialDevice.FromIdAsync(this.device.Id); // serialPort は SerialDevice
    this.serialPort.BaudRate = 9600;

    this.readStream = this.serialPort.InputStream.AsStreamForRead(0); // readStream は Stream
}


データの受け取りは特に何も考えず、雑に async void で非同期にして、while(true) でぶん回して待機しています*1
前述の通り、NMEA フォーマットは $ で始まって \r\n で終わるので、StreamReader にして ReadLine() で読むのも一つの手ですが、データが中途半端で返ってくる可能性を考えて 1 バイトずつ地道に受け取るようにしました。

public async void Listen()
{
    try
    {
        if (this.serialPort == null) return;

        while (true)
        {
            await ReadAsync();
        }
    }
    catch (Exception ex)
    {

    }
    finally
    {
        if (this.readStream != null)
        {
            this.readStream.Dispose();
            this.readStream = null;
        }
    }
}
private async Task ReadAsync()
{
    var buf = new StringBuilder();
    var readStart = false;
    var carriageReturn = false;
    while (true)
    {
        var c = (char)this.readStream.ReadByte();
        if (c < 0) break;

        if (c == '$')
        {
            if (readStart)
            {
                // リード中に $ がきたら異常と見なしてリセットする
                buf.Clear();
                readStart = false;
                continue;
            }

            readStart = true;
        }

        if (!readStart)
        {
            continue;
        }

        buf.Append(c);

        if (c == '\r')
        {
            carriageReturn = true;
        }
        else if (c == '\n' && carriageReturn)
        {
            break;
        }
        else
        {
            carriageReturn = false;
        }
    }

    var str = buf.ToString();
    await ParseAsync(str);
}


パースするデータは、Locapos に投げる最低限でいいので、緯度・経度・進行方位がわかる $GPRMC についてパースします。区切り文字が「,」なので、「,」で Split し、それぞれ解析します。
パースについては割と地道だったので、前述したコードも含めて gist にそのまま投げておきました。記事の最後に載せておきます。参考になれば。


これで GPS からの情報は扱えるようになりました。Windows 10 Mobile などと違い、UWP の Geolocator クラスでは扱えないものではありますが、解析することでそれっぽく使うことは可能です。
他のデータも解析してログとして残しておけば、Raspberry PiGPS ロガーになりますし、自分のように Locapos に代表される位置情報共有サービスに送信することで、例えば自動車の位置情報を自動で送信する端末へと変わります。
Locapos への接続についてはまた改めて記事を書こうと思います。

通信について

こうしてできあがって、Locapos などと通信したい場合、Raspberry Pi3 には SIM スロットなどがないため、外へ通信するための装置が別途必要です。
ということで SORACOM スターターキットを手に入れて、付属の 3G ドングル AK-020 を接続してみましたが、Windows 10 IoT Core ではすぐ使えないようです *2
Windows 10 IoT Core に含まれるデバイスドライバーでは 3G / LTE ドングル系のがほとんどないようです。

Hardware compatibility list | Windows IoT

そこで、次に考えたのは、手元にあって余ってた MR03LN を再利用する方法でした。
Atermモバイルルーターにはスリープ状態のとき Bluetooth LE アドバタイジング ビーコンを発していて、そこにペアリング要求をするとスリープが解除される「リモート起動」の機能があります。

休止状態から起動する(リモート起動)|Aterm®MR03LN ユーザーズマニュアル

UWP では Bluetooth LE のビーコンを拾ってペアリング要求をプログラム上からすることができるので、
ビーコンを探す、見つかればペアリング要求をし、インターネット接続が確立するまで待機、完了し位置情報を捕捉できていれば送信する という流れを作ることができます。

最初はこれで運用していましたが、MR03LN の特性かわかりませんが、長時間休止状態だった場合にビーコンを発しなくなるようで、いつまでも通信できないという状態が起きていたため、万が一のときに使えないという事態を考え、もう一度考え直すことにしました。


ということで見つけたのが、ピクセラの PIX-MT100 でした。

www.pixela.co.jp

USB 端子が生えていますが、こちらも前述の通り Windows 10 IoT Core では接続して使うことはできません。ただしこのデバイスは USB からの電源供給さえあれば WiFi を飛ばすことができるので、MR03LN のような問題は解決しました。

ということで

簡単…とはいえかなり長くなってしまいましたが、こんな感じで Windows 10 IoT Core でも GPS モジュールをシリアル接続できれば GPS を使っていろいろすることができます。
また、GPS モジュールはシリアル接続ですので、Windows 10 IoT Core だけでなく、USB-シリアル変換キットを使って PC などと接続して使うこともできますし、Windows 10 IoT Core 向けに作ったアプリを Windows 10 PC でも使うことができます。


自分の手元では、自動車に積んで運用しています。Bluetooth LE のビーコンを送信するデバイスをうまく利用して、問題のある状態を検知したりすることで、自動車盗の早期検知と防犯に役立てればと思っています。

*1:ちゃんとやるならもうちょっと考えたいところ

*2:コマンド叩いたりすればいけるのだろうか?