1. はじめに
GPSというと、多くの人はまず「現在地を知るためのもの」というイメージを持つと思います。実際、スマートフォンの地図アプリやカーナビでは、GPSは位置を特定するための仕組みとして使われています。
しかし、GPSは位置情報だけでなく、非常に正確な時刻も受信できます。むしろ仕組みの本質としては、衛星から送られてくる正確な時刻情報をもとに距離を計算し、その結果として位置を求めています。つまりGPSは、位置測位システムであると同時に、巨大な時刻配信システムでもあります。
今回、この仕組みを使って GPSマスタークロック を自作してみました。
構成としては、Windows PC に接続した GPS 受信機から COM ポート経由で NMEA 文を読み取り、FastAPI で現在の時刻や衛星情報を保持し、それを React 製のフロントエンドへ WebSocket で配信する形です。
画面は、左側に大きなアナログ形式の連続秒針時計、右側に受信中の衛星名や信号状態を表示するTUI風パネルを配置しました。単なるデバッグ画面ではなく、配信でそのまま使えるように、黒背景ベースの計器風UIとしてまとめています。なお、配信用途を意識して、緯度・経度などの座標情報は表示しない構成にしています。
この記事では、以下の内容を順番に紹介します。
- なぜ GPS で時刻がわかるのか
- GPS受信機からどのような情報が取得できるのか
- FastAPI で GPS 時刻・衛星情報を扱うバックエンド構成
- React + SVG で連続秒針のアナログ時計を実装する方法
- 実際に作ってみて分かったことやハマりどころ
また、今回使った受信機は 1PPS 信号が取れない構成 だったため、いわゆる超高精度な基準時計ではなく、NMEA から取得した GPS 時刻をもとに、見た目にも滑らかで実用的な時計を作る という方針で実装しています。そのため、厳密な時刻同期機器というよりは、GPSを活用したリアルタイム表示アプリケーションとして読むとイメージしやすいと思います。
GPS信号の取得、NMEAのパース、WebSocket配信、連続秒針の描画、そして配信向けUIの設計まで、一通りをまとめて追える内容にしていくので、これから似たような仕組みを作ってみたい人の参考になれば幸いです。
2. なぜGPS信号で時刻がわかるのか

GPSというと「位置を測る仕組み」という印象が強いですが、実際には時刻を極めて正確に扱う仕組みです。
GPSで位置が分かるのは、衛星から送られてくる正確な時刻情報を使って、電波が届くまでにかかった時間を計算しているからです。
GPS衛星には高精度な原子時計が搭載されており、各衛星は「この信号をこの時刻に送信した」という情報を常に電波に載せて送っています。受信機はその信号を受け取り、衛星が送信した時刻と、自分が受信したタイミングとの差をもとに、衛星までの距離を求めます。電波は光とほぼ同じ速度で進むため、時刻の差が分かれば距離が分かる、というのが基本的な考え方です。
たとえば、ある衛星が「12時00分00秒ちょうどに信号を送った」とします。受信機がその信号をわずかに遅れて受け取った場合、その遅れ時間に光速を掛けることで、その衛星までの距離を計算できます。GPSはこれを複数の衛星に対して行い、位置を割り出しています。
つまり、GPSで位置が求められるのは、もともと時刻を非常に正確に扱っているからです。
見方を変えると、GPSは「位置情報システム」である前に、「非常に正確な時刻を広域配信するシステム」でもあります。
さらにGPS受信機は、単に衛星との距離を計算するだけではなく、自分自身の内部時計のずれも同時に補正しています。地上の受信機に載っている時計は、GPS衛星の原子時計ほど正確ではありません。そのため、複数の衛星からの信号を使って位置を計算する過程で、「受信機の時計がどれだけずれているか」も一緒に解いています。これによって、受信機は現在時刻をかなり正確に知ることができます。
今回のようにGPS時計を作る場合は、位置そのものよりも、この受信機が補正して得た正確な時刻を利用します。GPS受信機はその時刻をNMEA文としてシリアル出力してくれることが多く、たとえば RMC や ZDA といった文には日時情報が含まれています。PC側ではこのNMEA文を読むことで、GPS由来の現在時刻を取得できます。
なお、少し厳密に言うと、GPS衛星が内部的に扱っているのはUTCそのものではなくGPS時刻です。GPS時刻はうるう秒を含まないため、UTCとは完全には一致しません。ただし、多くのGPS受信機はこの差を内部で補正して、NMEA出力ではUTCベースの日時を返してくれます。そのため、アプリケーション側では通常の時刻データとして扱いやすくなっています。
要するに、GPSで時刻がわかるのは、衛星が「自分の位置」ではなく、“この信号を送った正確な時刻” を絶えず送信しているからです。
そして受信機は、その正確な時刻をもとに位置を計算しながら、自分自身の時計も合わせ込んでいます。今回作ったGPSマスタークロックは、その仕組みのうち「位置」ではなく「時刻」の恩恵を使ったものです。
3. 今回作ったもの
今回作成したのは、GPS受信データをもとに、時刻同期の状態をリアルタイムで監視できる 「GPS Clockダッシュボード」 です。
単に時刻を表示するだけではなく、GPSから正しく時刻が取得できているか、測位状態は安定しているか、どの衛星を受信できているかまで、一つの画面で確認できるようにしました。
構成は大きく分けて、FastAPIによるバックエンド と React + Viteによるフロントエンド の2層です。
バックエンドがGPSデバイスからNMEA文を受信・解析し、フロントエンドがその状態を視覚的に分かりやすく表示します。
バックエンド構成(FastAPI)
バックエンドは、GPSデバイスからシリアル経由で流れてくるNMEA文を常時受信し、それを解析して内部状態として保持する役割を持っています。
受信対象は時刻情報だけではなく、測位状態や使用衛星数、捕捉衛星数、衛星ごとの信号情報なども含みます。
保持した状態は、以下の2つの手段で外部へ提供します。
- REST API による現在状態の取得
- WebSocket による状態更新のリアルタイム配信
REST APIでは、現在のGPS状態、衛星一覧、ヘルスチェック用の死活確認、生のNMEAの最新行などを取得できるようにしました。
一方でWebSocketでは、GPS状態の変化をフロントエンドへ即時にPush配信し、低遅延で画面に反映できるようにしています。
また、実運用を考えると、GPSデバイスが一時的に未接続になったり、受信が不安定になったりすることは十分あり得ます。そのため、GPS入力が止まった場合でもアプリケーション全体が落ちないようにし、「未接続」「受信なし」も状態として扱う 設計にしています。これにより、サーバー自体は継続稼働しつつ、フロント側から異常を切り分けやすくしています。
フロントエンド構成(React + Vite)
フロントエンドは、取得したGPS状態を配信向けに見やすく表示するためのダッシュボードです。
画面は大きく左右に分かれており、左側には連続秒針で動作するアナログ時計、右側にはTUI風のステータスパネルを配置しています。
右側のパネルには、以下のような情報を表示します。
- GPSの接続状態
- 測位状態
- UTC時刻とローカル時刻
- 使用衛星数・捕捉衛星数
- 受信中の衛星名
- 信号レベル
- クロックソース状態
初回表示時はREST APIから現在状態を取得し、その後はWebSocketでリアルタイムに追従更新する構成にしました。
さらに、WebSocket切断時には自動再接続を行い、衛星一覧については定期再取得することで、表示が自然に追従するようにしています。
時計表示は、バックエンドから受け取った時刻を単に1秒単位で描き直すのではなく、基準時刻と経過時間から補間して描画することで、滑らかな連続秒針になるようにしています。これにより、単なる監視画面というより、実際に配信でも使える見た目に仕上がりました。
4. 要件
今回のアプリは、単なる試作ではなく、実際に使い続けられることを意識して要件を整理しました。
要件は大きく、機能要件、非機能要件、そして配信向けの表示・運用ポリシー に分けて考えています。
4.1 機能要件
まず前提として、GPSデータを正しく受信できることが必要です。
そのため、COMポート経由でNMEA文を受信できること、また受信が停止した場合でもアプリケーション全体が落ちず、異常を状態として扱えることを要件にしました。
そのうえで、バックエンドには以下のようなデータ提供機能を持たせています。
- ヘルスチェック用の死活確認API
- 現在のGPS状態を返すAPI
- 受信衛星一覧を返すAPI
- 最新の生NMEAを返すAPI
また、監視用途としては、状態変化をできるだけ遅延なく反映できることが重要です。
そこで、GPS状態の更新をWebSocketでPush配信し、フロントエンドがそれを低遅延で画面に反映できるようにしました。
UIとしては、時計表示とGPS状態表示を同一画面で確認できることを重視しています。
さらに、LOCKED / HOLDOVER / NO FIX のような状態を直感的に判別できること、受信衛星や信号レベルを一覧で把握できることも重要な要件としました。
4.2 非機能要件
機能面だけでなく、日常的に扱いやすいことも重要です。
そのため、起動容易性として、スクリプト実行だけでバックエンドとフロントエンドをまとめて立ち上げられる構成を目指しました。
保守性の面では、バックエンドをルーティング、サービス、スキーマに分離し、役割ごとに整理した構成にしています。
また、主要ロジックであるNMEA解析、状態管理、ヘルスチェックについてはテストを用意し、動作確認しやすい形にしました。
可用性についても意識しており、GPSデバイスが未接続でもAPI応答自体は継続できること、クライアントが切断された場合でもWebSocket管理が破綻しないことを要件に含めています。
つまり、GPSが不安定でも「全部止まる」のではなく、状態として劣化を見せながらシステム自体は動き続ける ことを重視しています。
4.3 表示・運用ポリシー(配信向け要件)
今回は配信用途を前提にしているため、表示内容には明確な方針を設けました。
最も重要なのは、位置特定につながる情報を画面に出さないこと です。
そのため、以下の情報は表示しない方針にしています。
- 緯度
- 経度
- 高度
- 速度
- 方位
あわせて、視聴者にとって意味が薄く、かつ内部情報に寄りすぎるものも非表示にしています。
- COMポート情報
- 生NMEA全文
その代わり、配信で見て分かりやすい情報、つまり「時計として正しく動いているか」「GPS同期できているか」「どの衛星を受信しているか」に絞って表示するようにしました。
レイアウトや情報量も、デバッグ重視ではなく、配信時の視認性を優先した構成 にしています。
このように、今回のGPS Clockダッシュボードは、単なる受信確認ツールではなく、GPS時刻・衛星受信状態・同期状況を、配信に適した形でまとめて監視できるアプリとして設計しました。
5. システム構成
本システムは、GPS受信機から取得したNMEAデータをバックエンドで受け取り、必要な情報に正規化したうえで、REST APIとWebSocketを使ってフロントエンドへ配信する構成になっています。
役割分担はシンプルで、取得(GPS)→ 解析・配信(FastAPI)→ 表示(React) の3段階です。
GPS受信機は、シリアルポート経由でNMEA文を継続的に出力します。
NMEA文には、時刻、測位状態、使用衛星数、捕捉衛星数、衛星ごとの情報などが含まれており、バックエンドはこれを常時受信します。
FastAPIで構成したバックエンドは、受信したNMEA文を解析し、アプリケーションで扱いやすい形に整理します。
ここでは単に生の文字列を転送するのではなく、必要な項目を抽出して、現在のGPS状態としてメモリ上に保持します。保持対象は、たとえば以下のような情報です。
- 現在のUTC時刻
- ローカル時刻
- Fix状態
- 使用衛星数
- 捕捉衛星数
- 受信衛星一覧
- 衛星ごとの信号レベル
この状態を外部へ提供する手段として、REST APIとWebSocketの2系統を用意しました。
REST APIの役割
REST APIは、主に画面の初期表示や、必要に応じた明示的な再取得のために使います。
フロントエンドが最初に起動した時点では、まだWebSocket経由の更新を受け取っていない可能性があります。そのため、まずREST APIを使って「現在の状態のスナップショット」を取得し、初回表示を確実に行います。
本システムでは、REST APIを通じて以下のような情報を取得できるようにしています。
- 死活確認用のヘルスチェック
- 現在のGPS状態
- 受信衛星一覧
- 最新の生NMEA
このうち、配信用UIでは位置情報や生NMEA全文は表示しませんが、内部的には状態確認やデバッグに使えるようにしています。
WebSocketの役割
WebSocketは、状態の変化をリアルタイムでフロントエンドへ伝える ために使います。
GPSの受信状態や時刻同期状態は、運用中に少しずつ変化していくため、毎回REST APIをポーリングするよりも、バックエンド側から更新イベントをpushするほうが自然です。
バックエンドでGPS状態が更新されるたびに、その内容をWebSocket経由でReact側へ配信します。
フロントエンドはこれを受け取ることで、時計の状態表示、Fix状態、衛星一覧、信号レベルなどを低遅延で更新できます。
この方式により、監視画面として必要な即時性を確保しつつ、ポーリングのしすぎによる無駄な通信も抑えられます。
Reactフロントエンドの役割
フロントエンドは、REST APIとWebSocketを組み合わせたハイブリッド方式で動作します。
初回表示ではREST APIから現在状態を取得し、その後はWebSocketで更新を追いかける構成です。
この構成にした理由は、次の2点を両立したかったからです。
- 起動直後に「今の状態」を確実に表示できること
- 運用中の変化を低遅延で追従できること
もしWebSocketだけに依存すると、接続確立のタイミングによっては初期表示が空になる可能性があります。
逆にREST APIだけで更新を追いかけると、リアルタイム性が落ちたり、ポーリング設計が煩雑になったりします。そこで、初回はREST、以降はWebSocketという役割分担にしました。
全体のデータフロー
システム全体の流れを整理すると、以下のようになります。
- GPS受信機がシリアルポート経由でNMEA文を継続出力する
- FastAPIバックエンドがNMEA文を受信し、必要な情報を解析して現在状態を更新する
- フロントエンド起動時はREST APIで現在状態のスナップショットを取得する
- その後はWebSocketで更新イベントを受け取り、UIへ即時反映する
- Reactが時計表示やステータスパネルを更新する
文章だけで表すと、構成は次のようになります。
GPS受信機
↓ シリアル(COM)
FastAPIバックエンド
├─ NMEA解析
├─ 状態保持
├─ REST API
└─ WebSocket
↓
React + Vite フロントエンド
├─ アナログ時計
└─ TUI風ステータスパネル
この構成にした理由
今回の用途は、単にGPSデータを取得することではなく、配信向けの見やすい監視画面を作ること でした。
そのため、バックエンドはデータ取得と正規化に集中し、フロントエンドは表示に専念する構成にしています。
また、配信用途を前提にしているため、緯度・経度のような位置特定につながる情報はフロント側では表示しません。
一方で、「時刻同期できているか」「測位状態は安定しているか」「どの衛星を受信しているか」といった、視聴者にとって意味が伝わりやすい情報は前面に出しています。
このように、取得・解析・配信・表示の責務を分けたことで、実装の見通しが良くなっただけでなく、今後の拡張もしやすい構成になりました。
まとめ
本システムは、GPS受信機が出力するNMEAデータをFastAPIで受信・解析し、REST APIで初期状態を提供しつつ、WebSocketで更新をリアルタイム配信する構成です。
Reactフロントエンドはこの2系統を組み合わせることで、起動直後の確実な描画 と 運用中の低遅延な状態追従 を両立しています。
6. GPS受信機から取得できる情報
本システムでは、GPS受信機が出力するNMEA文のうち、代表的な RMC / GGA / GSV / ZDA を利用しています。
これらを組み合わせることで、単に「時刻が取れる」だけではなく、時刻同期の基準、測位状態の安定性、衛星の受信状況 まで把握できます。
今回のGPS Clockダッシュボードでは、配信用途のため位置情報そのものは画面に出していませんが、内部的にはこれらの文を解析することで、時計として信頼できる状態かどうかを判断しています。
6.1 RMC(Recommended Minimum Navigation Information)
RMCは、NMEAの中でも比較的よく使われる文で、最低限の航法情報をひとまとめにしたもの です。
時刻、日付、位置、速度、進行方位など、現在の状態をコンパクトに取得できます。
取得できる主な項目は以下の通りです。
- UTC時刻
- 日付
- 測位有効フラグ(有効 / 無効)
- 緯度・経度
- 速度(対地速度)
- 進行方位(コース)
- 磁気偏角(機種による)
RMCは、言ってしまえば「今のGPS状態をざっくり把握するための基本情報セット」です。
特に今回のようなGPS時計では、UTC時刻と日付を取得できる ことが重要で、これだけでも時計表示の基礎データとして使えます。
また、測位有効フラグを見れば、現在の時刻情報をどの程度信用してよいかの判断材料にもなります。
位置や速度、進行方位も取れますが、本システムでは配信時の表示対象からは外し、主に内部状態や確認用途に留めています。
6.2 GGA(Global Positioning System Fix Data)
GGAは、測位結果の品質を確認するために重要な文 です。
RMCが「現在の基本情報」を扱う文だとすると、GGAは「その位置解や時刻基準がどれだけ信頼できるか」を見るための文と言えます。
取得できる主な項目は以下の通りです。
- UTC時刻
- 緯度・経度
- Fix Quality(未測位 / GPS測位 / DGPS など)
- 使用衛星数
- HDOP(水平精度劣化率)
- 高度(平均海水面基準)
- ジオイド高
この文の中で特に重要なのは、Fix Quality と 使用衛星数 です。
現在きちんと測位できているのか、あるいは未測位なのか、どの程度の品質で解が得られているのかを判断できます。
今回のダッシュボードでは、LOCKED や NO FIX のような状態表示を分かりやすく出していますが、その判断材料としてGGAの情報が非常に役立ちます。
また、使用衛星数が少ない場合は、時刻や測位の安定性にも影響が出る可能性があるため、監視対象として重要です。
HDOPや高度なども取得できますが、配信用途では情報量が多くなりすぎるため、主表示には使わず、内部的な補助情報として扱う方針にしました。
6.3 GSV(Satellites in View)
GSVは、現在見えている衛星の詳細情報 を表す文です。
1文ですべてを表せない場合が多いため、通常は複数の文に分かれて送られてきます。
取得できる主な項目は以下の通りです。
- 視野内衛星総数
- 各衛星のPRN番号
- 仰角
- 方位角
- SNR(信号強度)
この文を使うことで、「今どの衛星が見えているのか」「信号の強さはどの程度か」を一覧化できます。
今回のダッシュボード右側にある受信衛星一覧や信号レベル表示は、主にこのGSVの情報をもとに構成しています。
GSVは、時計としての時刻同期そのものに直接使うというより、受信環境やアンテナ状態を可視化するための情報源 として役立ちます。
たとえば、受信衛星数が急に減ったり、SNRが全体的に低かったりする場合は、設置場所や受信状態に問題がある可能性が考えられます。
また、PRN番号から衛星系を識別すれば、GPSだけでなく、SBASやQZSS(みちびき)なども区別して表示できます。
この部分は見た目にも分かりやすく、今回のアプリではかなり「映える」ポイントになりました。
6.4 ZDA(Time and Date)
ZDAは、UTC時刻と日付の取得に特化した文 です。
RMCにも時刻と日付は含まれますが、ZDAはよりシンプルに「時刻基準」として使いやすい構成になっています。
取得できる主な項目は以下の通りです。
- UTC時刻(時分秒)
- 日付(年・月・日)
- ローカルタイムゾーン情報(送出される機器のみ)
今回のようにGPS時計を作る場合、ZDAはかなり扱いやすい文です。
余計な航法情報を含まず、時刻と日付を明確に取り出せるため、時刻同期や表示処理の基準 として使いやすいからです。
もっとも、実際には機種によってZDAを出すものと出さないものがあります。
そのため実装上は、ZDAがあれば優先的に採用し、なければRMCの日時情報を使う、といった柔軟な設計にしておくと扱いやすくなります。
6.5 今回のシステムでの使い分け
今回のGPS Clockダッシュボードでは、これらの文をそれぞれ次のように使い分けています。
-
RMC
基本となる日時情報や測位有効フラグの取得 -
GGA
Fix状態、使用衛星数、測位品質の確認 -
GSV
受信衛星一覧と信号レベルの生成 -
ZDA
時刻同期の基準となるUTC日時の取得
このように役割を分けることで、単一の文だけに依存せず、時刻・測位・衛星受信状況を多面的に監視できる 構成にしています。
6.6 まとめ
RMCで基本航法情報、GGAで測位品質、GSVで衛星受信状況、ZDAで時刻基準を取得することで、本システムは**「今どれだけ正確に測位・時刻同期できているか」** を複数の観点から監視できるようになっています。
GPS時計というと時刻だけを見ればよさそうに思えますが、実際にはその時刻がどのような状態で得られているかを把握することも重要です。
その意味で、NMEA文を複数組み合わせて扱うことには大きな意味があります。
7. NMEAの読み取りとパース
このシステムでは、GPS受信機から流れてくるNMEA文をバックエンドで継続的に受信し、文種ごとにパースして、最新状態を安全に保持する構成にしています。
実装上のポイントは大きく3つです。
- 止まらない受信
- 壊れないパース
- スレッドセーフな状態管理
単にGPSから文字列を読むだけであればそれほど難しくありませんが、実際にはポートの一時切断や不正データ、欠損値、GSVのような複数文構成などを考慮する必要があります。
そこで今回は、「多少揺らいでもアプリ全体は止まらない」ことを優先した設計にしました。
7.1 COMポート受信の流れ
GPS受信は、FastAPI本体とは分離したバックグラウンドスレッドで常時実行しています。
これにより、GPSデバイス側で問題が起きても、HTTP APIやWebSocketサーバー自体は継続稼働できるようにしています。
処理の流れは概ね次の通りです。
- FastAPI起動時に、GPS受信用バックグラウンドスレッドを開始する
- 設定されたCOMポートを指定ボーレートでオープンする
-
readline()で1行ずつNMEA文を受信する - ASCIIとしてデコードし、不正文字は置換して文処理へ渡す
- パース結果を最新状態へ反映する
- 更新後の状態をWebSocketで配信する
このように、受信・解析・状態更新・配信を一連の流れとしてつないでいます。
ただし責務自体は分離しており、受信スレッドは「ひたすらNMEA行を取り込む」、パーサは「行を構造化データへ変換する」、状態マネージャは「最新状態を一元管理する」という役割に分けています。
この方式のメリットは、APIサーバー本体とGPS受信処理を疎結合にできることです。
仮にGPSの受信側で再接続が必要になっても、ヘルスチェックや現在状態APIそのものは応答を継続できます。
7.2 パース処理の考え方
受信した1行のNMEA文は、まず文種を判定し、対応するパーサへ渡します。
今回利用している主な文種は、これまで説明した通り RMC / GGA / GSV / ZDA です。
実装では pynmea2 を利用しつつ、数値変換や欠損値処理については独自の安全変換関数をかませています。
これは、実際のNMEAでは空文字や不正値が混じることが珍しくないためです。単純に int() や float() を直接呼ぶと、そこで例外が発生して全体の受信ループに影響が出る可能性があります。
各文では主に以下の情報を取り出します。
-
RMC
時刻、日付、緯度経度、速度、方位 -
GGA
Fix品質、使用衛星数、HDOP、高度 -
ZDA
UTC時刻、日付 -
GSV
衛星PRN、仰角、方位角、SNR
このうち、RMCやZDAは時計用途の基準時刻として重要で、GGAは現在の測位状態を判断するのに役立ちます。
GSVは少し特殊で、1回の送信で完結せず、複数文で1セットの衛星情報を表現します。
そのためGSVについては、受信した文をその場で即反映するのではなく、一時バッファへ蓄積し、最終文に到達したタイミングでまとめて衛星一覧へ反映するようにしました。
こうすることで、途中までしか届いていない不完全な衛星一覧をUIへ出してしまう問題を防げます。
7.3 例外処理の設計
GPS受信では、正常系だけを前提にするとすぐ不安定になります。
ポートが開けない、読み取り中に切れる、不正なNMEAが混じる、といったことは普通に起こります。そこで例外処理は、受信停止を防ぐために層を分けて実装しています。
ポートオープン失敗時
まずCOMポートを開く段階で失敗することがあります。
たとえば、ほかのプロセスがポートを占有していたり、デバイスが一時的に認識されていなかったりする場合です。
この場合は次のような流れで対処します。
- 占有プロセスの解放を試行する
- 失敗時は利用可能な別ポート探索を試みる
- それでも接続できない場合は、待機後に再接続ループへ入る
このように、一度失敗したから即終了するのではなく、復旧可能性を前提に再試行する 方針にしています。
読み取り中エラー
ポートが開けた後でも、読み取り中にエラーが発生することがあります。
USBシリアルの抜き差しやデバイス再起動などが典型例です。
この層では、エラーの種類によって扱いを分けています。
- シリアル例外
→ 受信ループを抜けて、再接続処理へ移行する - 一般例外
→ ログを出しつつ、可能な限り受信継続を試みる
つまり、明らかに接続が壊れたケースでは再接続へ切り替え、軽微な異常ではその場で全停止しないようにしています。
文処理中エラー
NMEA文そのものの処理でも例外は発生し得ます。
たとえば、欠損フィールドが入っていたり、値が想定外だったり、途中で壊れた文字列が来たりする場合です。
ここでは1文単位で例外を握りつぶし、全体ループは継続する方針を取っています。
不正あるいは欠損したデータは None 扱いにし、必要に応じてその項目だけ破棄するか、取得できた部分だけ反映します。
この設計によって、たった1行の異常データのために受信全体が止まることを防げます。
7.4 状態管理の設計
パースした結果は、その場その場で直接APIレスポンスへ返すのではなく、インメモリの状態マネージャに集約しています。
この状態マネージャが、システム全体の「最新のGPS状態」を一元管理する中心になります。
更新時はロックで保護し、読み取り時はスナップショットを返すことで、受信スレッドとAPI応答、WebSocket配信の競合を避けています。
つまり、「書き換え中の途中状態をAPIが読んでしまう」といった問題を防ぐ設計です。
主な更新対象は以下の通りです。
- 時刻
- Fix状態
- 使用衛星数
- 捕捉衛星数
- 衛星詳細
- 速度
- 方位
- 各種測位関連情報
さらに、補助情報として以下も保持しています。
- 最終更新時刻(モノトニック時刻)
- 最終更新時刻(システム時刻)
- GPS接続可否
モノトニック時刻を持っておくことで、「最後にGPS更新が来てからどれくらい経過したか」を安全に評価しやすくなります。
これは、HOLDOVER状態の判定や、連続秒針用の基準時刻管理にも役立ちます。
この仕組みにより、REST APIは常に一貫した最新スナップショットを返せるようになり、WebSocket配信も同じ状態源を参照できるようになります。
つまり、RESTとWebSocketで見ている世界がずれない 構成になっています。
7.5 この設計にした理由
今回のGPS Clockダッシュボードは、単にGPSデータを取れればよいものではなく、配信や常時監視で使っていて止まらないこと が重要でした。
そのため、NMEA処理系では「理想的な入力だけを前提にしない」ことを強く意識しています。
- ポート接続で失敗しても再接続する
- 不正なNMEAが混ざっても全体は止めない
- GSVのような複数文データも不完全なまま表示しない
- API応答と受信更新が競合しないよう状態管理を統一する
こうした方針を積み重ねることで、GPS側に多少の揺らぎがあっても、監視UIそのものは安定して動き続ける構成にしました。
7.6 まとめ
NMEA受信はバックグラウンドスレッドで常時実行し、RMC / GGA / GSV / ZDA を文種ごとに安全にパースして、インメモリ状態へ統合しています。
例外はポート接続層、読み取り層、文処理層で段階的に吸収し、再接続ループとスレッドセーフな状態管理によって、GPS側の揺らぎがあっても監視UIを止めない実装にしています。
この設計があることで、次に説明するFastAPI側のAPI設計やWebSocket配信も、安定した状態管理の上に構築できるようになりました。
8. FastAPIバックエンドの構成
本プロジェクトのバックエンドは、責務を 「ルーティング」「サービス」「スキーマ」「配信」 に分離した構成にしています。
GPS受信のようなI/O処理と、APIとして外部に見せる入出力定義を切り離すことで、保守しやすく、拡張しやすい設計を目指しました。
GPS受信アプリは、一見すると「シリアルを読んでAPIで返すだけ」のように見えますが、実際には次のような要素が混在します。
- シリアルポートの継続監視
- NMEA文の解析
- 最新状態の一元管理
- REST APIの応答
- WebSocketによるリアルタイム配信
- クライアント切断や再接続への対応
これらを1か所に詰め込むと、変更しづらく、トラブル時の切り分けもしにくくなります。
そこで今回は、責務ごとにレイヤーを分けた構成にしました。
8.1 ルーティング層(APIエンドポイント)
ルーティング層は、HTTPおよびWebSocketの入口だけを担当します。
この層ではビジネスロジックを極力持たせず、エンドポイント定義と入出力の受け渡しに集中させています。
主な役割は以下の通りです。
- エンドポイント定義
- リクエスト受付とレスポンス返却
- サービス層から取得した状態の公開
- ヘルスチェックや現在状態取得など、用途ごとのルート分離
たとえば、/health は死活確認専用、/api/gps/current は現在のGPS状態取得、/api/gps/satellites は受信衛星一覧取得、といった形で責務を明確に分けています。
WebSocketについても同様で、接続受付と送受信の橋渡しだけを行い、状態そのものの更新はサービス層に任せています。
この分離の利点は、エンドポイントを追加してもコアロジックへ影響しにくいことです。
たとえば後から「診断用API」や「履歴取得API」を増やしたくなっても、既存の受信処理や状態管理へ手を入れずに拡張しやすくなります。
8.2 サービス層(業務ロジック)
サービス層は、バックエンドの中核処理を担当します。
具体的には、GPS受信、NMEA解析、状態更新、配信制御といった、アプリケーションの本質的な処理をここへ集約しています。
今回の構成では、主に次のようなサービスで成り立っています。
GPSリーダー
GPSリーダーは、COMポートからNMEA文を継続受信する役割を持ちます。
FastAPI本体とは別のバックグラウンド処理として動かし、ポート切断や読み取りエラーが発生しても、再接続を試みながら継続動作できるようにしています。
主な役割は以下の通りです。
- COMポートからの継続受信
- 切断時の再接続制御
- 受信エラー時の例外吸収
- 受信行をパーサへ渡す処理
NMEAパーサ
NMEAパーサは、受信した1行のNMEA文を文種ごとに解析し、内部で扱いやすい値へ変換します。
RMC、GGA、GSV、ZDAを中心に対応し、型変換や欠損値処理もここで吸収します。
主な役割は以下の通りです。
- RMC / GGA / GSV / ZDA の解析
- 数値変換や日時変換
- 欠損値や不正値の安全な取り扱い
- GSV複数文の統合
特にGSVは複数文で1セットになるため、途中結果を一時保持し、最終文到達時にまとめて衛星一覧へ反映するようにしています。
状態管理
状態管理サービスは、最新のGPS状態をインメモリで一元管理する役割を持ちます。
ここがバックエンド全体の「現在地」にあたる部分であり、API応答もWebSocket配信も同じ状態源を参照します。
主な役割は以下の通りです。
- 最新GPS状態の保持
- スレッドセーフな更新
- スナップショット取得
- API応答と配信処理の共通状態源
このように、サービス層に中核処理を集約することで、ルーティング層は薄く保ちつつ、ロジックの見通しをよくしています。
8.3 スキーマ層(データ契約)
スキーマ層は、APIで扱うデータ構造を明示し、入力と出力の契約を固定化する層です。
FastAPIではPydanticモデルを使うことで、レスポンスの形を明確に定義できるため、内部状態と外部公開データの境界をはっきりさせやすくなります。
役割は以下の通りです。
- GPS状態レスポンスの型定義
- 衛星情報の構造化
-
Optionalフィールドによる欠損データ吸収 - 内部データと外部公開データの境界明確化
たとえば、衛星情報であれば、PRN、衛星名、仰角、方位角、SNRといったフィールドを明示的に定義しておくことで、フロントエンド側は「どんなJSONが返ってくるのか」を安定して前提にできます。
また、GPSデータには null になり得る項目が多いため、スキーマでOptionalをきちんと表現しておくことが重要です。
これにより、「未測位時には値がない」「SNRが取れない衛星がある」といったケースも自然に扱えます。
結果として、フロントエンドとのデータ契約が安定し、変更時の影響範囲も追いやすくなります。
8.4 WebSocket配信
WebSocketは、GPS状態の変化をクライアントへpush配信するリアルタイム経路です。
REST APIが「今の状態を取りに行く」仕組みだとすれば、WebSocketは「変化したら即座に通知する」仕組みです。
処理の流れは概ね次のようになります。
- GPS受信・パース後に状態を更新する
- 更新後の最新状態を接続中クライアントへ配信する
- クライアント側は受信イベントごとにUIを更新する
- 切断されたクライアントは管理対象から除外する
このようにして、時計状態やGPS同期状態、衛星情報の変化を低遅延で画面へ反映できるようにしています。
また、運用上はクライアントの切断や再接続は普通に起こるため、接続中クライアントの一覧管理や、送信失敗時の切断処理も重要です。
ここを雑に実装すると、配信先の管理が壊れたり、送信例外で全体が止まったりするため、接続数追跡やクリーンアップも配信層の責務として扱っています。
8.5 RESTとWebSocketの役割分担
このバックエンドでは、初期取得はREST API、更新追従はWebSocketという役割分担にしています。
この構成にした理由は、表示の確実性と即時性を両立したかったからです。
- 初期表示
→ REST APIで現在状態を確実に取得する - 運用中の更新
→ WebSocketでリアルタイム追従する
もし初期表示までWebSocket任せにすると、接続タイミングによっては画面が空のままになる可能性があります。
一方で、更新まで全部RESTでまかなうと、ポーリングの負荷や遅延が気になります。
そこで、最初にRESTでスナップショットを取得し、その後の変化はWebSocketで受け取る構成にしました。
これは、実運用の監視UIとしてかなり扱いやすい方式です。
8.6 この構成にした理由
今回のバックエンドでは、「動けばよい」よりも、後から直しやすいこと、壊れにくいこと、役割が追いやすいこと を意識しました。
- ルーティング層は薄く保つ
- GPS受信や解析はサービス層へ集約する
- APIの契約はスキーマで固定化する
- リアルタイム更新はWebSocketに分離する
この構成にしておくと、たとえば将来的に次のような拡張もしやすくなります。
- SQLiteへの履歴保存
- 追加NMEA文への対応
- 衛星名変換ロジックの強化
- 複数クライアント配信の最適化
- 外部監視システムとの連携
責務分離を最初から意識しておくことで、試作で終わらず、あとから育てやすいコードベースになります。
8.7 まとめ
FastAPIバックエンドは、ルーティングを薄く保ち、GPS受信・NMEA解析・状態管理をサービス層へ集約し、スキーマでデータ契約を固定化した構成にしています。
さらに、WebSocket配信を組み合わせることで、初期取得はREST、リアルタイム更新はpush という実運用向けの役割分担を実現しました。
この構成により、GPS側の不安定さを受け止めつつ、フロントエンドへは一貫した状態を安定して提供できるようになっています。
9. 連続秒針の実装
配信画面の時計では、時刻が正しいことと同じくらい、見た目が安定していること も重要です。
そこで今回の時計では、秒針を「1秒ごとに飛ぶ表示」ではなく、毎フレームなめらかに進む連続秒針 として実装しました。
単に現在秒をそのまま表示するだけだと、機能としては時計になりますが、配信画面ではどうしてもカクつきが目立ちます。特に大きめのアナログ時計を表示する場合、この差はかなりはっきり見えます。
9.1 1秒ごとの更新がカクつく理由
まず、一般的な「秒単位更新」の時計がなぜカクついて見えるのかを整理します。
更新頻度が低すぎる
秒針を1Hz、つまり1秒ごとにしか更新しない場合、表示は1秒間その場で静止し、次の瞬間にまとめて次の位置へ移動します。
アナログ時計の秒針は1分で360度回るため、1秒あたりの移動量は次の通りです。
360 / 60 = 6
つまり、1秒ごと更新では毎回6度ずつ飛ぶ ことになります。
この6度という角度差は、アナログ時計として見るとかなり分かりやすく、目視でも段差移動がはっきり分かります。
画面の描画周期と合わない
一般的なディスプレイは60Hz前後で描画されます。
つまり、1秒のあいだに約60フレーム描画されるのに、秒針の角度はその間ずっと変わらず、1秒後にだけジャンプすることになります。
このため、視覚的には「連続して動いている時計」ではなく、静止と急変を繰り返す時計 に見えてしまいます。
特に今回のように、黒背景の大きな計器風UIでは、秒針の動きが強く視線を集めるため、カクつきは余計に目立ちます。
配信圧縮でさらに不自然になる
動画配信では映像が圧縮されるため、微小な変化よりも「急な変化」のほうが目につきやすくなります。
その結果、秒針がなめらかに動く場合よりも、静止 → 急変 のパターンのほうが不自然に見えやすくなります。
ローカル画面ではそれほど気にならなくても、配信映像になると急に「カクついて見える」と感じやすいのはこのためです。
9.2 どう補間したか
この問題に対して、今回は秒を整数で扱わず、ミリ秒まで含めた連続値として角度を計算する方式を採用しました。
考え方はシンプルで、現在秒を次のように表します。
s = seconds + \frac{milliseconds}{1000}
たとえば「12秒と500ミリ秒」であれば、秒針の計算上は 12.5秒 として扱います。
そのうえで、秒針の角度を次の式で求めます。
\theta_{sec} = 6 \times s
秒針は1秒あたり6度進むため、この式で連続的な角度が得られます。
これを毎フレーム再計算して描画すれば、秒針は1秒ごとの段差ではなく、滑らかに回転しているように見えるようになります。
9.3 毎フレーム更新の方法
フロントエンドでは、ブラウザの描画タイミングに同期して処理を行える仕組みを使い、毎フレーム時計の角度を更新しています。
これにより、60Hz環境であれば実質的に毎秒60回程度、秒針の位置を更新できます。
ここで重要なのは、「1秒ごとに新しい角度を決めて、その間を見た目だけ補間する」のではなく、各フレーム時点の現在時刻から毎回角度を再計算することです。
この方式なら、表示の滑らかさを保ちながら、時刻の正しさも担保しやすくなります。
9.4 実装上のポイント
基準は毎回「現在時刻」から再計算する
角度更新は、前フレームとの差分を積み上げるのではなく、絶対時刻から都度再計算する方式にしています。
差分加算方式は実装が簡単に見える一方で、少しずつ誤差が積み上がり、長時間動かすとドリフトの原因になります。
一方、絶対時刻ベースなら、その時点の時刻から直接角度を計算するため、累積誤差が起こりにくくなります。
配信向けの時計として長時間動かすことを考えると、この方式のほうが安定します。
タブ非アクティブ復帰時の追従が速い
ブラウザでは、タブが非アクティブになると描画更新が抑制されることがあります。
差分加算方式だと、この間の停止時間を正しく考慮しないと、復帰時に時計が遅れたり、ぎこちない動きになったりします。
今回の方式では、復帰後もその時点の現在時刻から直接角度を求めるため、再開時に自然に正しい位置へ追従できます。
この点も、実運用で安定して見せるうえで重要でした。
GPS時刻表示との役割を分ける
今回の時計では、表示品質 と 時刻の正しさ を分けて考えています。
- 表示品質
→ フロントエンドで毎フレーム補間し、連続秒針として描画する - 時刻の正しさ
→ バックエンドから受け取るGPS由来の時刻を基準にする
つまり、見た目はブラウザ側で滑らかに作りつつ、基準となる時刻そのものはGPS同期済みの値を使う、という分担です。
この役割分離によって、視覚的な美しさと基準時刻の信頼性 を両立しています。
9.5 分針・時針も滑らかにする
秒針だけでなく、分針や時針も同じ考え方で連続的に動かしています。
秒針だけ滑らかで、分針と時針がカクカク進むと、全体としてどこか不自然に見えてしまうためです。
たとえば分針は、分だけでなく秒の影響も含めて計算します。
m = minutes + \frac{s}{60}
分針の角度は次のように求められます。
\theta_{min} = 6 \times m
時針も同様で、時に分や秒の影響を加えて連続値として扱います。
h = hours + \frac{m}{60}
\theta_{hour} = 30 \times h
こうすることで、時計全体が自然に回転しているように見え、計器としての完成度も上がります。
9.6 まとめ
秒針を1秒周期で更新すると、1回あたり6度の段差移動になり、配信画面ではカクつきがかなり目立ちます。
そこで今回は、秒をミリ秒まで含む連続値として扱い、毎フレーム角度を再計算して描画する方式に変更しました。
この方式により、視覚的に滑らかな秒針表現 と 時刻表示の追従性 を両立できました。
また、差分加算ではなく絶対時刻ベースで計算することで、ドリフトを防ぎつつ、タブ復帰時にも自然に追従できる実装になっています。
次は、この連続秒針を含むフロントエンド全体を、どのように計器風UIとして組み立てたかを見ていきます。
10. Reactフロントエンドと計器風UI
フロントエンドは、配信画面での視認性を最優先にして、左に時計、右に状態パネルを固定した2カラム構成にしました。
狙いはシンプルで、時刻の見やすさ と 運用監視情報の読み取りやすさ を同時に満たすことです。
単に情報を並べるだけであれば、1カラムのダッシュボードや表形式の一覧でも成立します。
ただ、今回は「配信で見せること」も前提にしていたため、ひと目で意味が伝わる画面構成が必要でした。
そこで、主役となる時計を大きく見せる領域と、状態確認のための情報パネルを分離し、それぞれに明確な役割を持たせています。
10.1 2カラム構成にした理由
画面構成としては、左側を大きなアナログ時計、右側を文字情報中心のステータスパネルとしています。
この構成にした理由は、アナログ時計と状態監視情報では、向いている表現方法がまったく違うからです。
時計は、現在時刻を瞬時に把握できることが重要です。
そのため、数字を細かく読むよりも、針の位置を直感的に捉えられるアナログ表示のほうが配信画面では強いと考えました。
一方で、測位状態や受信衛星数、信号レベルのような情報は、アナログ表現よりも文字情報のほうが向いています。
そこで右側には、短時間で状態判断しやすいように、TUI風のパネルをまとめて配置しました。
結果として、
- 左側で「今何時か」を直感的に見る
- 右側で「GPS同期が安定しているか」を定量的に確認する
という役割分担がはっきりした画面になりました。
10.2 左カラム: 計器風アナログ時計
左側は、このUIの主役となる時計表示です。
円形ダイヤルと針を中心に構成し、一般的な壁掛け時計というよりは、放送機材や計測機器のメーターに近い“計器感” を持たせるデザインにしています。
背景は暗め、針や目盛りは高コントラストで視認性を確保しつつ、全体としては黒ベースの無骨な雰囲気に寄せました。
これにより、配信画面の一部として置いても存在感があり、かつ情報機器らしい説得力のある見た目にできます。
連続秒針
時計表示で特に重視したのが、連続秒針 です。
前章で説明した通り、秒針は毎フレーム更新で滑らかに進むようにしており、1秒ごとの段差移動によるカクつきを避けています。
この効果は配信映像で特に大きく、静止と急変を繰り返す秒針よりも、連続的に動く秒針のほうがはるかに自然に見えます。
また、時計自体を大きく表示しているため、針の動きの質感は画面全体の印象にも大きく影響します。
状態の重ね表示
時計の近傍には、GPS LOCKED / HOLDOVER / NO FIX のような状態を小さく重ねて表示しています。
これにより、時刻を見る視線の延長で、現在の同期状態も同時に確認できるようにしました。
たとえば、時計だけが表示されていると、視聴者や運用者から見ると「この時計は本当にGPS同期しているのか」が分かりません。
そこで、時計そのものの周辺に状態を置くことで、時刻確認と同期状態確認を同じ視線移動の中で完結させています。
これは単なる装飾ではなく、配信画面としての意味づけにも役立っています。
見た目の時計であると同時に、「GPS由来の時刻を監視している画面」であることが自然に伝わるためです。
10.3 右カラム: TUI風ステータスパネル
右側は、文字情報を中心とした監視パネルです。
デザインはグラフィカルなカードUIというより、ターミナルや機器監視画面に近いTUI風 に寄せています。
この方向にした理由は、GPS状態の監視では、派手な装飾よりも一瞬で読める情報密度が重要だからです。
見出しごとにブロックを分け、必要な値を短く整理することで、短時間で状態を把握しやすくしています。
主なセクションは以下の通りです。
SYSTEM STATUS
ここでは、GPS接続状態やFix状態など、システム全体の総合状態を表示します。
たとえば、GPSデバイスが接続できているか、測位が成立しているか、現在時刻がどの程度信頼できるか、といった情報をここで確認できます。
このセクションは、右パネルの中でも最優先の情報領域です。
細かい衛星情報を見る前に、まず「そもそも今まともに動いているか」を判断できるようにしています。
RECEIVING SATELLITES
このセクションでは、現在受信中の衛星を一覧で表示します。
衛星名やシステム種別を並べることで、「今どの衛星が見えているか」を直感的に確認できます。
特に今回のUIでは、GPSだけでなく、みちびきなどが見えている場合にも名前が分かるようにしており、視覚的にも面白いポイントになっています。
単なる数字の羅列よりも、「どの衛星を受信できているか」が見えることで、受信している実感がかなり増します。
SIGNAL LEVEL
ここでは、各衛星の信号強度を把握するための指標を表示します。
数値だけでもよいのですが、可能な範囲でバー表現も併用することで、強弱が一目で分かるようにしました。
この情報は、時刻そのものを見るうえでは直接必須ではありませんが、受信状態の健全性を把握するためには非常に有用です。
たとえば、衛星数が多くても全体的にSNRが低ければ、アンテナ環境や設置場所に課題があるかもしれません。
CLOCK SOURCE
このセクションでは、現在のクロック参照元や運用状態を表示します。
GPS同期中なのか、一時的に受信が止まっているため保持状態なのか、といった運用上の意味をここに集約しています。
時計は見た目としては常に動いていても、その基準がGPSなのか、直近の保持状態なのかは重要です。
そのため、単に時刻が見えるだけでなく、その時刻の背景にある状態も分かる ようにしています。
10.4 UI上の役割分担
このフロントエンドでは、時計とパネルに明確な役割分担を持たせています。
-
時計
直感的に時刻を把握するための領域 -
パネル
定量的に状態を監視するための領域
つまり、時計は「見る」ためのUI、パネルは「読む」ためのUIです。
この役割を分けたことで、画面全体の情報整理がしやすくなり、視聴者にも運用者にも分かりやすい構成になりました。
もしこれらを1つの領域に混ぜてしまうと、時計の存在感が薄れたり、逆に監視情報が読みにくくなったりします。
今回の2カラム構成は、そうした情報の衝突を避けるうえでも効果的でした。
10.5 配信向け非表示ポリシー
このUIでは、「何を表示するか」だけでなく、何を表示しないか も明確に決めています。
配信時の安全性と実運用性を優先し、位置特定や機器内部情報につながる項目は意図的に非表示にしました。
非表示にしている主な項目は以下の通りです。
- 緯度
- 経度
- 高度
- 速度
- 方位
- COMポート情報
- 生NMEA全文
これらは技術的には取得できる情報ですが、配信画面に載せる意味はあまり大きくありません。
むしろ、視聴者にとっては不要な詳細であり、場合によっては位置特定や内部構成の露出につながる可能性もあります。
そのため、今回はあえて出さず、運用者が本当に必要とする“状態監視”に情報を集中させる設計にしました。
10.6 このUI方針にした理由
配信画面では、情報を多く出せばよいわけではありません。
むしろ、重要でない情報が増えるほど、必要な情報が埋もれて見にくくなります。
今回のUIで重視したのは、次の3点です。
- 時刻がひと目で分かること
- GPS同期状態がすぐ分かること
- 受信衛星の状況が短時間で把握できること
そのために、時計は大きく、状態パネルは簡潔に、そして不要な情報はあえて隠す方針を取りました。
結果として、技術的な情報量を確保しつつも、配信画面として過密になりすぎないバランスにできたと思っています。
10.7 まとめ
Reactフロントエンドは、左に計器風アナログ時計、右にTUI風ステータスパネルを配置した2カラム構成とし、直感的な時刻把握 と 定量的な状態監視 を両立しました。
また、配信運用を前提に、位置特定や機器内部情報につながる項目は非表示とし、視認性と情報安全性を優先したUIポリシーを採用しています。
この方針により、単なる技術デモではなく、実際の配信や常時監視にも使いやすい画面構成にできました。
11. ハマりどころ
実装時に特に詰まりやすかったのは、機器I/O、測位待ち、リアルタイム通信、表示用データ整形 の4点でした。
いずれも「正しく動いているとき」は目立ちませんが、実際の運用では不安定要素になりやすい部分です。
ここでは、それぞれについて 何が問題になったか、なぜ起きたのか、どう対策したか という順で整理しておきます。
このあたりは、GPSやリアルタイムUIを扱う実装ではかなり実践的なポイントでした。
11.1 COMポートまわり
問題
まず最初にぶつかりやすいのが、GPS受信機をつないでいるのにCOMポートが開けない、あるいは途中から読めなくなるという問題です。
シリアル通信は一見単純そうに見えますが、実際には環境依存の揺らぎがかなりあります。
主な原因
原因として多かったのは、次のようなものです。
- 別アプリが同じCOMポートを占有している
- 設定したポート番号が実機とずれている
- ボーレートが一致していない
- USBの抜き差しでポート番号が変わる
とくにUSBシリアル機器は、差し直しや再認識でポート番号が変わることがあり、昨日まで COM3 だったものが、今日は別番号になっている、ということも珍しくありません。
対策
この問題に対しては、「ポートが開けないことを異常系ではなく通常系として扱う」 方針にしました。
具体的には次のような対策を入れています。
- 起動時にポートオープン失敗を前提とし、再接続ループを持たせる
- 利用可能ポートを列挙して、必要ならフォールバック可能にする
- ポートが開けなくてもAPI自体は継続し、
gps_connected = falseの状態を返す - ログには「どのポートを何bpsで開こうとしたか」を必ず残す
重要なのは、「GPSが読めない = サーバー全停止」にならないことです。
今回のような監視UIでは、読めないなら読めないなりに状態として見せ続けることのほうが大事でした。
11.2 GPS未測位(No Fixが続く)
問題
次によく詰まるのが、NMEA自体は受信できているのに、Fixがなかなか有効にならない というケースです。
シリアルの読み取りは成功しているため、一見「動いている」ように見えますが、実際には測位が成立しておらず、時刻や位置の信頼性が低い状態が続きます。
主な原因
原因としては、主に次のようなものがあります。
- 屋内や遮蔽物の影響で衛星捕捉が弱い
- 初回コールドスタートで捕捉に時間がかかる
- アンテナの設置条件が悪い
特に初回は、受信機が十分な衛星情報を得るまで時間がかかることがあり、「NMEAは来ているのにずっと No Fix」という状態になりやすいです。
対策
この問題に対しては、UIと状態管理の両方で工夫しました。
- 「受信できている」 と 「測位できている」 を分けて表示する
-
satellites_in_viewとsatellites_in_useを別指標として見せる -
No Fixを即異常扱いせず、待機状態として扱う - 検証時は屋外、窓際、十分なウォームアップ時間を確保する
この分離はかなり重要でした。
単に「GPS connected」だけを見せると、利用者は「もう同期できている」と誤解しやすくなります。
そのため、受信の成立とFixの成立は別の状態として表現し、まだ測位待ちなのか、きちんと同期済みなのか を区別できるようにしました。
11.3 WebSocket再接続
問題
フロントエンド側では、Wi-Fiの揺らぎやバックエンド再起動でWebSocketが切れ、そのまま画面が古い状態で止まる という問題がありました。
リアルタイムUIでは見た目が更新され続けているように見えても、実際には最新状態を受け取れていない、というのがかなり危険です。
主な原因
主な原因は次の通りです。
- クライアント側で切断検知後の再接続処理が弱い
- 再接続間隔が固定で、失敗が続くと無駄なリトライが増える
- 再接続後の初期同期が不足している
単に「WSが切れたら再接続する」だけでは不十分で、復帰後に最新状態を再取得して同期を取り直す ところまで考える必要がありました。
対策
対策としては、次のような実装を入れています。
- 切断時は自動再接続を行う
- 再接続成功時に現在状態をREST APIから再取得する
- 接続状態バッジを表示し、「今更新できているか」を可視化する
- 将来的には指数バックオフも入れて安定性を上げる
ここで重要だったのは、「再接続できたこと」と「状態同期が取れたこと」は別問題 だと割り切ることでした。
再接続後にRESTで現在状態を取り直すことで、画面が過去のまま残る問題を避けられます。
また、ユーザーから見て「今リアルタイム更新が生きているのか」が分からないのも困るため、接続状態そのものをUI上で見えるようにしました。
11.4 衛星名変換(PRN → 人間可読名)
問題
衛星一覧を出そうとしたときに意外と厄介だったのが、GSVに出てくるPRN番号だけでは、どの測位系のどの衛星なのかが分かりにくい という点です。
たとえば G01 ならまだGPSだと分かりやすいですが、SBASやQZSS系は受信機の実装によって扱いが揺れることがあり、そのまま表示しても人間には意味が伝わりにくいことがあります。
主な原因
主な原因は次の通りです。
- Talker IDとPRN範囲の解釈が受信機依存で揺れる
- QZSSやSBASがGPS拡張PRNのように見える場合がある
- 表示名辞書が不足すると
Unknownが増える
つまり、仕様上は整理されていても、実機の出力が必ずしも「人間に分かりやすい形」にはなっていないことが問題でした。
対策
この点については、変換処理を少しずつ育てられるようにしました。
- Talker IDベース判定に加えて、PRN範囲で補正する
- QZSSの通称名辞書を持ち、段階的に拡充する
- 未知値は
Unknownで落とさず、そのまま表示継続する - UIでは最低限 「システム名 + PRN + SNR」 で判別できるようにする
ここで大事だったのは、辞書が不完全でもUI全体を壊さない ことです。
理想的には「みちびき○号機」まできれいに出したいですが、まずは未知値でも止めずに一覧を出し続けることを優先しました。
結果として、完璧ではなくても「今どんな衛星を拾っているか」は十分分かるようになり、実運用にはかなり役立つ表示になりました。
11.5 まとめ
ハードウェア連携では、COMポートの不安定さ、GPSの測位待ち、WebSocketの断続、衛星名変換の揺らぎが主なハマりどころでした。
いずれも共通していたのは、正常系だけを前提にするとすぐ壊れる という点です。
そこで本システムでは、
- 失敗を前提にした再試行
- 接続状態と測位状態の分離表示
- 未知値や欠損値でも止めない設計
に寄せることで、GPSや通信の揺らぎがあっても、監視システム全体は止まりにくい構成へ改善しました。
この考え方は、今回のGPS Clockに限らず、外部機器連携やリアルタイム監視UIを作るとき全般に有効だと思います。
12. 今後の改善
現時点でも、このシステムはGPS時刻の監視用途として十分に動作しています。
ただし、実運用や配信で長時間使っていくことを考えると、まだ改善したい点はいくつかあります。
特に今後強化したいのは、衛星表示の分かりやすさ、時刻精度そのもの、配信時の運用しやすさ の3点です。
ここでは、現状の課題と改善方針を整理しておきます。
12.1 みちびき通称名の強化
現状
現時点でも、PRNから衛星名への変換処理は実装しています。
そのため、衛星一覧上で単なる番号ではなく、ある程度は人間が読める形で表示できています。
ただし、実際には受信機や文種によって表記ゆれや判定揺らぎがあり、特にQZSSやSBAS系では「どの衛星として見せるのが自然か」が完全には安定していません。
その結果、衛星一覧は見られるものの、運用中にぱっと見で理解しづらい場面が残っています。
改善方針
この点については、次のような改善を考えています。
- QZSSの衛星名辞書を拡充し、運用上よく使う呼称へ統一する
- Talker IDだけでなく、PRNレンジと組み合わせて判定精度を上げる
- 名称未対応時は
Unknown固定ではなく、候補システムを推定表示する - 表示名と内部識別子を分離し、将来の命名変更にも強くする
つまり、内部では安定した識別子を持ちつつ、UI上では分かりやすい通称を表示する、という二層構造に寄せていく想定です。
期待効果
この改善により、衛星一覧の可読性が上がり、受信品質トラブル時の切り分けがしやすくなります。
特に、みちびきが見えているかどうかを即座に判断しやすくなるため、監視画面としての価値も上がるはずです。
12.2 PPS対応による時刻精度向上
現状
現状のシステムでは、GPS受信機から出力されるNMEA時刻を基準に、表示や配信を行っています。
この方式でも実用上は十分ですが、秒境界の厳密な精度という観点では、シリアル伝送遅延やOSスケジューリングの影響を受けます。
つまり、時計としてはかなり正確でも、「秒が切り替わる瞬間」まで厳密に合わせる という用途にはまだ限界があります。
改善方針
より高精度な時刻基準を扱うために、今後はPPS(Pulse Per Second)対応も検討しています。
改善の方向性としては、次のようなものを考えています。
- PPS信号を取り込み、秒境界をハードウェア基準で補正する
- NMEAは日時情報、PPSは位相基準として役割分担する
- 補正オフセットを計測・記録し、状態として可視化する
- 将来的にはNTPサーバ機能と連携し、配信先の時刻基準として活用する
この構成にすると、NMEAだけでは難しかった「いつ秒が変わったか」の精度を、ハードウェア寄りの基準で扱えるようになります。
期待効果
PPS対応が入ると、時刻同期の信頼性がさらに上がり、時計表示の正確性をより厳密に担保できるようになります。
単なるGPS表示アプリから一歩進んで、時刻基準そのものを扱うシステム としての完成度が高まるはずです。
12.3 OBS向け配信UXの改善
現状
現状でも、配信向けに不要な情報を隠し、時計と状態監視を両立した画面にはできています。
ただし、実際の配信運用を考えると、シーン構成や回線変動時の見え方、レイアウトのバリエーションなどには、まだ改善の余地があります。
特に、配信では「正常時に見やすいこと」だけでなく、不調時にも画面が破綻しないこと が重要です。
改善方針
今後は、OBSなどの配信ツールと合わせた使い勝手をさらに高めたいと考えています。
具体的には、次のような改善を予定しています。
- OBS向けレイアウトプリセットを追加する
例: 16:9、縦配信、情報量少なめ版など - 接続断続時の表示を強化し、視聴者に違和感の少ないフェイル表示へ寄せる
- 再接続ロジックを指数バックオフ化し、断続回線での安定性を上げる
- 文字サイズ、色コントラスト、余白を配信圧縮前提で最適化する
- 必要に応じて、自動シーン切替用の状態出力を用意する
ここでは単なる機能追加というより、配信現場で使い続けたときの運用負荷を下げること を重視しています。
期待効果
この改善により、配信中の運用負荷が下がり、トラブル時にも画面品質を保ちやすくなります。
また、視聴者から見ても「止まったように見える」「壊れたように見える」といった違和感を減らしやすくなります。
12.4 まとめ
今後は、みちびき通称名の辞書強化によって監視画面の可読性を高め、PPS連携によって時刻精度を底上げし、さらにOBS運用に最適化した表示や再接続制御を追加していく予定です。
こうした改善を積み重ねることで、このシステムを単なる可視化ツールで終わらせるのではなく、配信現場で長時間安定運用できる時刻監視基盤 へ発展させていきたいと考えています。
