Issue #66(P6-22)の正本仕様。PC上のシミュレータとAndroid Emulator上のDrogger Tabletを
別ウインドウで同時表示し、実圃場上の入力操作と製品画面の反応を並べて検証する。
利用者がPCシミュレータで現在地・走行経路・測位状態を操作しながら、Drogger Tabletの実際の画面で
現在地、実績軌跡、測位品質、stale、切断・再接続、ナビゲーション表示を同時に確認できるようにする。
Android別APKを同一Emulator内で切り替える旧構成では、操作結果を見るたびにアプリを往復する必要があり、
走行開始や測位品質変更の結果が操作画面から分からなかった。本仕様ではPCとEmulatorを2ウインドウで
常時表示し、この問題を構造的に解消する。
PCシミュレータ(独立ウインドウ)
地図・筆選択・現在地指定・経路編集・走行制御
│ 127.0.0.1:47650
│ request/response TCP
▼
adb forward tcp:47650 tcp:47650
│
▼
Android Emulator(独立ウインドウ)
Drogger Tablet debug専用TCP Service
│
▼
RegisteredRouteEngine → 疑似NMEA → 製品コードの受信・解析・表示・記録
BluetoothNmeaController、NMEA parser、FixRepository、ViewModel、地図、記録を:simulator-protocolの座標・経路・補間・走行Engine・payload検証を再利用する。利用前に次を実行する。
/home/akira/android-sdk/platform-tools/adb -s emulator-5554 forward tcp:47650 tcp:47650
PCシミュレータは127.0.0.1:47650へ接続する。Drogger Tablet debug ServiceもEmulator内の
127.0.0.1:47650だけで待ち受け、LANへ公開しない。
PC画面には少なくとも次を文言で表示する。
切断中も未送信ドラフトと地図カメラを保持する。再接続後にDrogger Tabletの最終地点が変わっていた
場合は、未送信経路を勝手につなぎ直さず確認を求める。
length(4 byte, big endian) + UTF-8 JSONで送る。id、protocolVersion、type、payloadを持つ。id、status、成功payloadまたは型付きerrorを持つ。初版で次を提供する。
| type | 意味 |
|---|---|
get_protocol_version |
protocol互換性確認 |
query_end_point |
登録済み経路の最終地点取得 |
query_run_state |
PC UIの走行制御パネル・測位/通信パネル向けのRunState・現在速度・経路有無・現在位置・測位品質・stale状態・activeLink有無の照会(Issue #66 P6-22 Gate 5走行制御・Gate 5第3区切り、debug限定) |
set_current_position |
経路を作らず、指定地点を現在地兼次回開始点として設定 |
submit_initial_route |
現在登録経路がない状態へ初回経路を登録 |
append_route |
取得済み最終地点から続く経路を追加 |
start_or_resume |
疑似走行を開始または再開 |
pause |
現在位置を保って一時停止 |
reset_progress |
登録経路の先頭へ進捗を戻す。記録や経路は削除しない |
set_speed |
疑似走行速度変更 |
set_fix_quality |
FIX/FLOAT/DGNSS/単独/測位なし |
set_stale |
NMEA送出停止・再開 |
trigger_communication_break |
疑似NMEA通信断を1回発生 |
始点だけを入力した状態では主操作を「現在地として設定」とする。これは1点の偽経路として扱わず、
set_current_positionでDrogger Tablet内部の現在位置を設定する。
Stage Bで追加した緯度・経度の数値入力は、PC Web shellからdebug TCPを通して
set_current_positionを実行できることを確認するための暫定UIであり、完成版シミュレータの主操作には
しない方針を採った。Track #68 Gate 4でこの数値入力フォームは廃止し、PC地図に置いた1点だけの
ドラフトが「現在地として設定」を兼ねる(5.3節)。完成版ではPC地図に実筆polygonを表示し、利用者が
地点・直線・曲線を入力し、PC側で展開したwaypoint列をDrogger Tabletへ登録する。走行開始後はその
経路に沿って連続的な疑似NMEAを生成する。
したがって、記録中に数値入力で遠隔地へテレポートする特殊ケースを通常の検証フローとして扱わず、
そのためだけに「複数.dtrkを一つの論理記録として束ねる」データモデルを導入しない。
set_current_position(Gate 4以降はPC地図の1点だけのドラフトから送信)は、通信確認またはsplitTrajectorySegmentForPositionJump()と関連するsegment/file分割は採用保留とし、set_current_positionの位置アンカー契約は保持する。start_or_resumeで走行させ、Tablet側の現在地・PCウインドウは地図を主領域とし、右または下に操作パネルを置く。Androidの800×1340固定骨格は
適用しない。PCの入力装置を素直に使う。
Ctrl+Z/Ctrl+Shift+Z(ドラッグ1回・クリック1回を1操作とする)DeletePC地図は「筆を選択」「経路を入力」の排他的な2ツールを持つ。クリック1つに複数の意味(筆選択・点配置等)
を持たせると、カーソルだけでは次に何が起きるか予告できなくなるため、以前の想定(「左クリックで筆・
開始点・直線終点・編集対象をまとめて選択」「入力/編集・直線/曲線の2状態トグル」)は撤回し、
ツールを明示的に切り替える方式にした。
筆を選択ツール(既定)
経路を入力ツール
ツール切替
V(筆を選択)、P(経路を入力)、Space押下中は一時的にpanモードEscは未確定操作を解除し、無ければ「筆を選択」へ戻る登録済み地点と現在地変更
set_current_positionの対象とし、GET /api/statusで登録済み地点を取得できれば同じ操作を開始できる。1点だけのドラフトの意味の区別(SinglePointDraftOrigin)
登録済み地点から自動採用した1点(利用者はまだ地図をクリックしていない)と、利用者が明示的に置いた
1点(「現在地を変更」ツール、または登録済み地点が無い状態での「経路を入力」の最初のクリック)は、
DraftRouteの点数だけでは区別できない(どちらも点数1)。区別できないと、自動採用しただけの1点が
誤ってset_current_positionの対象として送信可能になってしまう。そのためrouteとは別に
SinglePointDraftOrigin("current_position_change" / "route_start_pending_destination")を
状態として持ち、undo/redoでも一緒に復元できるようrouteとペアのスナップショットとして履歴へ積む
(DraftHistory<T>をgeneric化)。1点ドラフトの主操作は次のとおり決まる。
"current_position_change": 主操作は「現在地として設定」(set_current_position)。"route_start_pending_destination": 主操作は無効表示("地図をクリックして行き先を指定してください")。「現在地を変更」ツール
「筆を選択」「経路を入力」「経路を編集」に加える第4のツール。キーボードショートカットは割り当てず、
ボタンだけで到達する(5.1節の既存方針を維持)。クリックした地点だけを1点の現在地変更ドラフトへ
常に置き換える(既存ドラフトの有無を問わない)。このツールへの遷移は、未送信ドラフトが「失うものを
持つ」間は破棄確認パネルを挟む(判断は点数とSinglePointDraftOriginの組で行い、登録済み地点から
自動採用しただけの1点は「失うものが無い」として確認無しで静かに片付ける)。この確認は、Tabletの
既存登録経路を破棄するset_current_positionの2段階確認とは別物であり、混同しない。
登録済み地点の自動採用とundo履歴
登録済み地点をドラフト開始点として採用する処理(「経路を入力」ツールへ入った時点、GET /api/status
反映時、「続きを入力」ボタンの3箇所すべてが同じ関数を通る)は、undo履歴に積まない。これは利用者が
能動的に行った操作ではなく、ツールへ入った時点の基底状態であるため、履歴に積むと最初のUndoで
ドラフトが不自然に空へ戻り、その後の地図クリックが登録済み地点を無視した任意の開始点配置として
扱われてしまう。あわせて、この自動採用の直前と、送信成功でドラフトが空へ戻った直後(start_or_resume
成功・set_current_position成功のいずれも)にundo履歴を空にする。履歴を残したままにすると、
無関係な旧ドラフト(送信済み・別セッションのもの)のスナップショットが新しいセッションのundo/redoに
紛れ込む。
marker表示
登録済み地点とドラフト開始点が同一座標になる場合、緑pin([renderStartMarker])は表示せず、登録済み
地点marker側にregistered-endpoint-marker--draft-start修飾classを付けて、マゼンタの外周+緑の中心を
持つ複合markerへ切り替える。あわせて「Tablet登録済み地点」「ドラフト開始点」双方の文字表示にも
互いの関係を注記する。一致判定はroute・registeredEndpointStateのどちらが変化しても再計算する
(renderRouteから両markerを毎回まとめて再描画する)。
「筆を選択」「経路を入力」に加え、経路上の点・曲線区間の形状ハンドルをドラッグ編集する第3のツール
「経路を編集」を追加した。キーボードショートカットは割り当てず、画面のボタンだけで到達する
(5.1節の「ボタン操作だけで全操作が完結する」方針を維持する)。
hit test
arcLengthMidpoint)。操作
Space押下中は場所に関わらず常に地図パン(点・ハンドルの上でもドラッグを横取りしない)。点移動
DrawnCurve)の場合、区間内の中間点列を新しい弦(始点・終点をremapCurveToNewChord)。共有点(前後区間の境界)を動かしても、隣接する区間は同じ点IDを参照し曲線形状ハンドル変形
applyShapeHandleDelta/applyShapeHandleDeltaToPoints)。start→中間点列→endを結ぶ折れ線に沿ったsin(πt)で求める。弦投影方式は、手描きドラッグの実際のノイズや強く曲がる・sampledPoints)が0点になりうる。選択の視覚表示
ドラッグとundo/redo
historyには積まない。マウスを離した時点で実際にEscは仕掛かり中のドラッグ(曲線入力・点移動・ハンドル変形のいずれか)があれば、その場で表示と送信の一致
toExpandedPoints()(表示にも将来のTablet送信payloadにも使うmovePoint/applyShapeHandleDelta)はDraftRouteのGate 3の対象外
「筆を選択」「経路を入力」「経路を編集」で作ったドラフトを、下部(map-panel)の単一の主操作から
Drogger Tabletへ送る。Stage Bで追加した緯度・経度の数値入力フォーム(4.3.1節)は本Gateで廃止し、
地図で置いた1点のドラフトが「現在地として設定」を兼ねる(数値入力は完成版の主操作にしないという
4.3.1節の方針どおり)。
主操作(強い操作は常に1つだけ、docs/standards/ui-design-guidelines.md)
set_current_position)。submit_initial_routeまたはappend_route)とstart_or_resumeを一連の操作として実行する。登録済み地点がある状態で通常の「経路を入力」を選んだ場合、ドラフトはその登録済み地点を1点目として
自動生成する。したがって地図の最初のクリックで2点以上となり、走行経路が成立する。任意地点を1点だけ
置く「現在地として設定」は、明示的な「現在地を変更」操作からだけ到達させる。
Gate 4では通信契約を個別に検証するため、2点以上の主操作を「Drogger Tabletへ送る」として経路登録だけを
行い、走行開始を分離して実装した。しかし利用者検収で、この文言と挙動は「Tabletへ予定経路を渡したが
走行していない」という製品には存在しない状態を通常操作として露出し、PCの灰色破線とTabletの実績軌跡の
違いを理解しにくくすることが分かった。完成版の通常操作には採用せず、Gate 5で上記の操作体系へ変更する。
内部protocolでは経路登録と走行開始を引き続き分離する。これは登録直後・開始前、pause、再開、通信失敗などを
個別に自動テストするために有益だが、通常利用者へ別々の主操作として見せる理由にはしない。
送信種別の判定
クリック直後、pollingの3秒間隔に頼らずGET /api/statusを再取得し、常に最新のTablet登録済み
最終地点で判定する(判断条件「切断・再接続時、Tablet最終地点が送信前から変わっていたら送信を
止めて明示する」)。positionAnchor(4.3節)が設定されている場合もTablet側queryEndPointが
透過的にEndPointQueryResult.EndPointとして返すため、PC側は区別しない。
no_route): submit_initial_route。END_POINT_MATCH_TOLERANCE_M=SimulatorProtocol.ktと同じ値をTypeScript側routeSubmission.tsにもappend_route。expectedEndPointには照会済みのTablet最終地点をそのままset_current_positionの明示確認
set_current_positionはTabletの既存登録経路(またはpositionAnchor)を破棄する操作のため、送信直前に
取得した最終地点がno_routeでない場合は、1回目のクリックで確認文言(既存最終地点の座標・新しい
現在地の座標の両方を含む)を表示し、2回目のクリックで初めて送信する。ドラフトを変更(commit・
undo・redo)すると確認状態は破棄する。
送信データと速度
waypointは必ずtoRouteWaypoints()(4.3節「表示と送信の一致」と同じ、toExpandedPoints()が正)から
生成する。waypointへ速度・時刻は追加せず、速度は独立したset_speedで設定する。2点以上の通常操作では
有効な速度入力を必須とし、set_speed→submit_initial_route/append_route→start_or_resumeの順に実行する。
経路登録まで成功して走行開始だけが失敗した場合、同じ経路を再送して重複登録せず、「経路は登録済み・
走行開始に失敗」と表示して開始だけを再試行できるようにする。
HTTP API
既存POST /api/set-positionと同じenvelope規則(status:"ok"+outcome:"accepted"/"rejected"は
200、transport失敗は502)で、POST /api/submit-initial-route({"waypoints":[{"lat":..,"lon":..},..]})
とPOST /api/append-route({"expectedEndPoint":{"lat":..,"lon":..},"waypoints":[...]})を追加した。
JVM側([RouteSubmissionModels.kt])はブラウザ側検証を信用せず、waypoint・expectedEndPointの型
(数値であること、NaN/Infinity拒否)と緯度経度の範囲だけを検証し(400)、最小点数・
MAX_SUBMISSION_WAYPOINTS超過・経路連続性(expectedEndPoint不一致)はTablet側RegisteredRouteEngineの
ドメイン結果(RouteSubmissionRejection、200+outcome:"rejected")へ委ねる(SetPositionModels.ktと
同じ役割分担: JVM層は型・範囲だけ、業務規則はTablet往復)。malformed JSON・Content-Type不正・
body上限超過・method不正はそれぞれ400/415/413/405。
送信・走行開始成功後
outcome:"accepted"のTablet最終地点を、pollを待たず接続card(「登録済み最終地点」)へ即時反映する。set_current_positionで登録経路を破棄した場合は、走行対象で経路のみ登録する特殊試験
submit_initial_route/append_routeだけを呼ぶ機能は、登録直後・走行開始前の状態を検証する内部API・
自動テスト契約として保持する。完成版の通常UIには「経路のみ登録」ボタンを置かない。将来、利用者が
開始タイミングを厳密に同期する必要が確認された場合だけ、「詳細な試験操作」へ追加する。
拒否・通信エラー・切断
RouteSubmissionRejection)は握り潰さず表示する。EndPointMismatchは期待値・実際値Gate 4の対象外とGate 5の実装境界
start_or_resume/pause/reset_progress、速度変更、測位品質・stale・疑似通信断の統合、Android
シミュレータAPK/AIDLの削除はGate 4では扱わない。Gate 5では最初に、上記の「2点以上=速度指定して
即時走行開始」への主操作変更と、破棄済み灰色破線の消去を実装・検収した。pause/resume/resetは
5.4節のとおり実装した。測位品質・stale・疑似通信断の統合は5.4節の利用者検収完了後に着手する。
常時確認する情報は次とする。
現在位置・進捗をPC地図へ常時同期することは初版の必須条件にしない。走行開始後は隣のDrogger Tablet
ウインドウで実際の移動を見る。PC側には少なくとも「走行開始を受理しました。Drogger Tabletで確認」
と表示する。
一時停止・再開・進捗を先頭へ戻す、をPC UIの「走行制御」パネル(地図パネルの下)から操作する。
RunStateの取得
既存/api/status(query_end_pointと同じ接続で送るquery_run_state)を拡張し、次を追加で返す。
runState: not_started / running / paused / finishedspeedMps: 現在設定速度hasRoute: 走行可能な区間(RegisteredRouteEngine.route.segments)が1つ以上あるか。set_current_positionの現在地アンカーだけ(区間0件)はfalseになる。currentPosition: 現在位置または進捗位置(緯度経度のみ、経路全形状は返さない)query_end_pointが返す最終地点(判断条件「必要以上に経路全形状を返すAPIへ拡張しない」)とは別の
query typeとして追加した。CONNECTED以外、またはquery_run_state自体が失敗した場合(古いTablet
APK等でTabletがcommandを認識しない場合を含む)は4フィールドともnullになり、PC UIは「状態取得
失敗」表示へフォールバックする(接続自体が失敗した「接続なし」とは区別する、判断条件「Tabletと
PCの不一致時に安全に拒否する」)。protocol versionは変更していない(Stage Aで確立した「既存
requestを壊さない追加はversion不変」方針を継続、10.1節参照)。
PC UIの状態表示
「走行状態」badgeで次を文言表示する(色だけで状態を表さない)。
| 表示 | 意味 |
|---|---|
| 状態確認中… | /api/statusのpolling結果がまだ確定していない(接続card側の「確認中」と同じ判断) |
| 接続なし(Tablet未起動/protocol不一致含む) | 接続自体が失敗している |
| 状態取得失敗 | 接続はあるがquery_run_stateに失敗した(PCとTabletのビルド不一致等) |
| 経路未登録 | 接続・RunState取得は成功したが、走行可能な区間が無い(hasRoute=false) |
| 未開始 | NotStartedかつ経路あり |
| 走行中 | Running |
| 一時停止中 | Paused |
| 終了 | Finished |
状態別操作
NotStarted+経路あり): 主操作は「走行開始」(start_or_resumeをそのまま送る、内部的にはRunningになるため、reloadや経路のみ登録の内部試験後にだけ到達する。Running): 主操作は「一時停止」のみ。再開ボタンは同時表示しない。Paused): 主操作は「走行を再開」(同じstart_or_resume)。「進捗を先頭へ戻す」も有効。Finished): 主操作は表示しない(badge文言「終了」で状態だけを示す)。「進捗を先頭へ戻す」だけ「進捗を先頭へ戻す」ボタンの近くに常に次を表示する。
進捗を先頭へ戻します。経路・速度・Tabletの記録は削除しません。記録中は戻り先までの直線が実績
軌跡に現れる場合があります。
走行中のresetは対象外とした判断
RegisteredRouteEngine.reset()自体はRunStateに関わらず呼び出し可能だが、PC UIはRunning中の
「進捗を先頭へ戻す」を無効のままとした(位置ジャンプの確認表示を別途作り込むより、一時停止を経由
させる操作導線の方が誤操作を防げるため)。「一時停止中または終了後だけ有効とする」という正本が
明示的に許容する単純化を採用した判断であり、Track #68のtrack_recordに記録する。
pause中もNMEA送信を継続する
pauseはDrogger Tablet内部のRegisteredRouteEngine.runStateをPausedにするだけで、経路進捗・
現在位置は変更しない。RegisteredRouteEngine.shouldEmitSimulatedNmeaはPausedをRunning/
Finishedと同列に扱い常に送出するため(4.3.1節と同じ「送出可否は経路登録の有無ではなくRunState」
方針)、pause中も同じ位置のGGA/RMCを送信し続ける。Tabletがstaleになることはない。
resetの意味の維持
reset_progressは登録経路・記録済み軌跡のどちらも削除せず、内部の走行進捗だけを経路先頭へ戻す
(10.3節・12節の既存契約を維持、変更していない)。reset単体では走行を自動再開しない。
reload/reconnect後の復元
PC UIのブラウザ状態(lastRunState/lastHasRoute等)は推測値として扱わず、reload・reconnectの
どちらでもGET /api/statusから取得したTablet側RunStateを正として復元する。PC側の推測状態と
Tablet状態が食い違う場合はTabletを正とする(3.2節の既存方針と同じ)。
HTTP API
POST /api/pause・POST /api/resetを追加した(bodyは持たない)。応答envelopeは既存
POST /api/start-or-resumeと同じ規則(status:"ok"+outcome:"acknowledged"で200、
transport失敗は502)。操作成功後はpollingを待たず/api/status`を再取得し、期待するRunStateへ
実際に遷移したことを確認してからUI表示を確定する(通信応答だけで断定しない、判断条件「pause失敗時に
Pausedと表示しない」等)。
表示と記録・進捗の違い
7節の区別に加え、この節の3操作を次のとおり位置付ける。
一時停止(pause): 疑似車両の経路進行を止める。Tabletの記録・実績軌跡には触れない。再開(start_or_resume): 停止地点から進行を再開する。経路を再送しない、速度を再送しない。進捗を先頭へ戻す(reset_progress): 走行進捗だけを経路先頭へ戻す。経路・速度・Tabletの記録の測位品質・stale・疑似通信断をPC UIの「測位・通信」パネル(走行制御パネルの下)から操作する。3操作は
それぞれ独立した意味を持ち、混同しない(4.2節「重要な意味の区別」を参照)。
測位品質
NMEA送信は継続したまま、送信内容のGGA fix quality/RMC statusを変える操作(set_fix_quality、既存)。
排他的な選択UIとし、次の5値を扱う(SimulatedFixQuality、SyntheticNmeaGenerator.kt)。
| PC UIの表示 | 送信するGGA fix quality | Tablet側の表示(PositionQualityBadge、p6-ui-spec.md) |
|---|---|---|
| 固定解(FIX) | 4 | 固定解 |
| 浮動解(FLOAT) | 5 | 浮動解 |
| 差分測位(DGNSS) | 2 | 差分測位 |
| 単独測位(SINGLE) | 1 | 単独測位、またはNTRIP接続済みなら補正受信中...(Tablet側のNTRIP接続状況に依存し、PCからは制御しない) |
| 測位なし(NONE) | 0(座標欄も空、buildGgaSentence) |
測位無効(PositionQuality.INVALID、fixValid=false分岐。stale専用の「情報が古い」とは別区分) |
PC UIのラベルは、Tablet正本(docs/specifications/p6-ui-spec.md「常時表示すべき情報」節の表)の用語
(固定解/浮動解/差分測位/単独測位)をそのまま使う。「測位なし(NONE)」は操作名としてPC側が付けた名前
であり、Tablet側の実際の表示は「測位無効」になることをラベル自体に明記する(判断条件「実際のTablet
表示と用語が異なる場合は、正本・製品コードへ合わせる」、PC側で独自名称は作らない)。
要件と実装:
query_run_stateが返すfixQualityから表示する(バッジ+選択中ボタンの強調表示、positioning.tsのdecideFixQualityDisplay)。runControlSubmitting、走行制御と共有、後述)は5ボタンとも無効化する。/api/statusを再取得してfixQualityが要求値と一致することを確認してから成功表示にする。lastFixQuality/api/statusの値)だけから導出するため、失敗時にlastFixQualityを更新しなければdoSetFixQualityAction、main.ts)。/api/statusのfixQualityから復元する(ブラウザ側にpending選択状態を持たない)。SimulationController.configのqualityフィールドだけを更新する既存set_fix_qualityのTCP実装をそのまま使う)。SimulationController.configはSimulatedNmeaLinkの送出可否と独立した状態のため、既存Controller契約上は制約がない)。ただしparseSetFixQualityRequestBody(PositioningModels.kt)が400で安全に拒否するSimulatedFixQuality.valueOfのIllegalArgumentExceptionを捕捉、例外を投げっぱなしにしない)。stale(NMEA送出停止・再開)
set_stale(既存)による、疑似NMEA送信そのものの停止/再開。「一時停止」という文言は使わない(走行の
pauseと混同するため)。2状態の操作として実装する。
要件と実装:
staleを有効にした直後から「Tabletは、最終受信から約5秒(正本DEFAULT_STALE_THRESHOLD)経過すると
『情報が古い』状態になります」と表示する(positioning.tsのSTALE_THRESHOLD_EXPLANATION)。5秒という
値はAndroid側正本定数DEFAULT_STALE_THRESHOLD(app/src/main/java/dev/drogger/tablet/model/ Staleness.kt)そのものであり、PC側にstale判定用の別定数は持たない(この文言は表示専用で、staleかどうか
の判定自体は常にTablet側だけが行う)。
stale解除で経路をresetしない、start/resumeを呼ばない、pause中に有効化した場合も解除後は同じ停止地点の
NMEAへ戻る。これらは既存SimulatedNmeaLink.setPaused(pausedフラグでNMEA送出だけを止め、
RegisteredRouteEngineの経路・進捗・RunStateには一切触れない)の既存契約のまま(4.3.1節相当の
Pause/NONE/staleの区別は10.3節・5.4節の既存記述どおり、変更していない)。
利用者検収指摘と修正(重要): 初版実装はpaused中に一切データを送らず、BluetoothNmeaControllerの
watchdog(15秒無通信をCOMMUNICATION_LOSSとみなす、実機と共通の安全機構、DEFAULT_WATCHDOG_TIMEOUT_MS)
が発火してNmeaLinkFactory.connectを呼び直し、新しいSimulatedNmeaLink(既定paused=false)へ
差し替わってしまうため、利用者が明示的に「NMEA送信を再開」を押していないのに約15秒でstaleが勝手に
解除される不具合があった(検収不合格、通信接続自体を維持するという要件にも反する)。原因は
「stale=完全な無通信」という実装がTrack #64由来のwatchdog(実機と共有する生存監視)と衝突すること。
修正は2点(いずれもSimulatedNmeaLink/SimulationControllerに閉じ、BluetoothNmeaController
・RegisteredRouteEngineは変更していない)。
buildStaleHeartbeatSentence()SyntheticNmeaGenerator.kt、$PDGGRHB*hhのような、GGA/RMC/GSTのいずれのsentenceTypeともBluetoothNmeaController.lastLineAtMsを更新し続けることでwatchdogのFixRepository.ingestSentenceはGGA/RMC/GST以外を無視する(lastReceivedAtだけfixValid/fixQuality/lastValidFixAt(=isStale()の判定対象)には触れず、SimulationController.staleRequested(新設)に明示要求を保持し、新しいSimulatedNmeaLinkは初期paused値としてこれを引き継ぐ。上記1で通常はwatchdog自体を防げるが、SimulatorTcpRequestHandlerのSetStale分岐がTCP request受理と回帰test: appのSimulationIntegrationTest(watchdog閾値超過後もConnected/staleを維持、明示的な
setPaused(false)でのみ解除、疑似通信断からの再接続後もstale要求を引き継ぐことを実際の
BluetoothNmeaController状態機械で検証)。
request成功後、/api/statusを再取得してstaleEnabledが要求値と一致することを確認してから成功表示に
する。失敗時はlastStaleEnabledを更新しないため、バッジ・ボタン文言は変更前のまま保たれる
(判断条件「stale開始/解除失敗時に先走って表示を変えない」)。
pauseとstaleは別状態として保持する(RunState.PausedとSimulatedNmeaLink.staleEnabledは独立した
フィールド、5.4節「pause中もNMEA送信を継続する」の既存契約と整合)。NONEとstaleも別状態
(fixQuality=NONEは送信内容の話、staleは送信の有無の話)。
接続なし・protocol不一致では操作不可(positioningActionsEnabled)。
activeLink(疑似NMEA送出中のSimulatedNmeaLink)が無い場合(TabletのBluetooth接続先が「NMEA走行
シミュレータ」に未設定)、requestは無害なno-opとして成功するが、PC UIはパネル上部の共通警告
(active-link-message)でその旨を表示する。
疑似通信断
trigger_communication_break(既存)による、Tablet製品側の切断・再接続処理を1回発生させる操作。
ボタン「疑似NMEA通信断を発生」の付近に、PC→debug TCP接続を切る操作ではないこと、経路・速度・進捗を
削除しないことを明記する。
製品側の初回自動再接続は即時であり、そのままではTablet UIが切断状態を描画する前に復帰するため、
debugシミュレータのNmeaLinkFactory.connectだけを約3秒待機させる。製品側の
BluetoothNmeaControllerの切断検知・再接続方針は変更しない。
通信断中も疑似車両は走行を継続する(重要、Track #68 Gate 5第4区切り): 実車はNMEA通信が切れても
走り続ける。したがって疑似通信断は「NMEA送信の停止」だけを再現し、RegisteredRouteEngineの走行進捗は
RunState.Runningである限り指定速度で進み続ける。通信断中の中間座標は生成・送信せず、再接続後は
通信断していた実経過時間×速度だけ進んだ位置からNMEA送信を再開する(Tabletの実績軌跡は通信断前後の
測位点を直線で結ぶことがある)。PC UIの疑似通信断説明文(index.html)にもこの契約を明記する。
runControlSubmitting共有)。/api/statusの再照会は行わない利用者検収指摘と修正(重要): 初版実装(Gate 5第3区切り、観察可能な約3秒待機を追加した版)は
NMEA送出tickerと経路進捗更新が同じcoroutineに結び付いていたため、意図せず2つの問題を抱えていた。
BluetoothNmeaControllerはCOMMUNICATION_LOSS再接続時に旧NmeaLinkのclose()を呼ばない(実socketは既に死んでいる前提、production方針)。SimulatedNmeaLinkはこのclose()だけを契機にtickerを止めていたため、疑似通信断発生後も旧SimulatedNmeaLinkのtickerがRegisteredRouteEngine.advance()を二重に修正(いずれもSimulatedNmeaLink/SimulationControllerに閉じ、BluetoothNmeaController・
RegisteredRouteEngineは変更していない)。
SimulatedNmeaLink.simulateCommunicationLoss()が自身のtickerJobを即座にcancel()する。これによりSimulationControllerに疑似通信断開始時の単調時計時刻(System.nanoTime、consumeCommunicationBreakElapsedSecondsでSimulatedNmeaLinkのinitが、この実経過RegisteredRouteEngine.advance()をまとめて呼ぶ(SimulationRuntime.lock内、advance()自体がRunState.Running以外では何もしないため、Paused/NotStarted中の回帰test: appのCommunicationBreakCatchUpTest(Running/Paused/NotStarted/終端到達/二重加算防止/
NMEA非送出/catch-up後の送出位置/経路・速度・stale要求の保持を、SimulationController.nanoTimeProvider
差し替えによる決定論的な経過時間注入で検証)、SimulationIntegrationTest(実際のBluetoothNmeaController・
parser・FixRepositoryを通した既存の約3秒間の非Connected状態・自動復帰testに、catch-up後の進捗・
NMEA再開位置の検証を追加)。
接続・操作競合
runControlSubmittingmain.ts、走行制御アクション・測位/通信アクションupdateRunControlPanel()/updatePositioningPanel()を相互に呼ぶ)。positioningActionsEnabled)。SimulatorStatusServiceのlock(1操作=1 TCP接続、呼び出し全体を直列化)をそのまま再利用し、debug専用状態照会の拡張(reload/reconnect後の復元)
既存/api/status(query_run_state)は測位品質・stale状態を返していなかった(調査結果)。PC UIの
reload/reconnect後復元に必要な最小限の3フィールドを、新しいTCP query typeを追加せず既存
query_run_state/RunStateSnapshotへ追加する形で拡張した(判断条件「経路全形状やTablet製品UI内部状態
まで返す大きなAPIにはしない」、既にPC UI復元用のqueryとして存在するものへの最小追加が最も単純なため)。
fixQuality: 現在のシミュレーション設定(SimulationController.config.value.quality)。activeLinkのstaleEnabled: NMEA送出を停止中か(SimulatedNmeaLink.staleEnabled、既存pausedフィールドのactiveLinkPresent: 疑似NMEA送出中のSimulatedNmeaLinkが存在するか。既存SimulatorTcpResult.OneWayApplied.activeLinkPresentと同じ意味で、set_stale/trigger_communication_breakの成否とは独立に照会できる(再接続中状態を安全に取得できる既存値のactiveLinkPresentはBluetooth接続先の選択状態を表すものであり、通信断イベントの発生有無を表すprotocol version(SIMULATOR_PROTOCOL_VERSION)は変更していない(10.1節「既存requestを壊さない追加は
version不変」方針を継続、Gate 5走行制御でのRunStateSnapshot拡張と同じ扱い)。PCが本Gateのビルド、
Tabletが本Gateより前のビルドの組み合わせは、query_run_state自体は成功する(古いRunStateSnapshotの
4フィールドは既存どおり)ため、追加した3フィールドのJSON欠落時にPC側decodeが例外を投げうる制約が残る
(既存のhasRoute/currentPosition等も同じ制約を持ち、単一リポジトリでPCとTabletを同一commitから
ビルドする開発運用を前提としている、5.4節と同じ判断)。query_run_state自体を認識しない、より古い
Tablet(Gate 5走行制御より前)は既存どおりUnknownCommandとなり、PC UIは「状態取得失敗」表示へ
フォールバックする(3フィールドを含む4フィールド全体がnullになる、5.4節の既存動作を維持)。
必須テスト(この節の実装確定に合わせて追加したもの)
simulator-protocol: TcpJsonCodecTestにfixQuality/staleEnabled/activeLinkPresentの往復・app(debug): SimulatorTcpRequestHandlerTestに、activeLink無しでのfixQuality反映と、実際にSimulatedNmeaLinkを起動した状態でのstaleEnabled/activeLinkPresent反映を追加。SimulationIntegrationTestに、stale中はwatchdog閾値超過でも切断・自動再開しないこと、明示的なsetPaused(false)でのみ解除されること、疑似通信断で再接続してもstale要求(staleRequested)をBluetoothNmeaController状態機械で検証する回帰testを追加(検収指摘対応、pc-tool: PositioningModelsTest(品質・staleのJVM側入力検証、未知品質値の安全な拒否)、SimulatorStatusServiceTest・SimulatorUiHttpServerTestにsetFixQuality/setStale/triggerCommunicationBreakと/api/set-fix-quality//api/set-stale//api/communication-breakのsimulator-ui(TypeScript): positioning.test.ts(表示・ラベル・許可判定の純粋関数)を新設。必須テスト(Gate 5第4区切り、疑似通信断の走行進捗catch-up)
app(debug): CommunicationBreakCatchUpTest(新設)。Running中の速度別catch-up距離、Paused/SimulationController.nanoTimeProvider(テスト専用の単調時計差し替え)による決定論的なSimulationIntegrationTestの既存の約3秒間BluetoothNmeaController・parser・FixRepositoryを通した進捗・NMEA再開位置のsrc/debugだけに置く。advance()を同じlock/dispatcherで直列化する。Drogger Tabletのピンク線は予定経路ではなく記録済みの実績軌跡である。PCシミュレータの予定経路とは
別物として扱う。
記録開始/停止: 実績軌跡を保存する操作start_or_resume/pause: 疑似車両を動かす内部操作。通常UIでは2点以上の経路送信と最初のstart_or_resumeを「この経路で走行を開始」へまとめるreset_progress: 走行進捗を戻す操作set_current_position: 現在地を指定する操作軌跡をクリア: Drogger Tablet地図上の実績軌跡を消す操作これらを同じ「開始」「Reset」で曖昧に表さない。
adb -s emulator-5554を使う。127.0.0.1:47650とし、任意host入力を設けない。現状(Track #68): 以下のPC UIを含む一連操作は利用者検収済みで、試験運用可能とする。試験専用ツール
であるため、既知制約を除去し切ることを完了条件にせず、実利用で再現した問題を個別に記録・修正する。
上記「通信prototype」はStage A(Track #67)で完了し、以下のStage B PC地図UIもTrack #68で実装・検収した。
Stage Bでは、走行制御パネルだけを先に完成させるのではなく、旧Androidシミュレータ
(Track #65)で保持した実筆polygon、筆選択、開始点・直線終点・手描き曲線、編集、undo、表示経路と
送信waypointの共通化という成果をPC Web UIへ移植する。地図経路入力から連続走行まで成立した状態を
「PCでDrogger Tabletの動作を検証できる」最小の利用者検収境界とする。測位品質、stale、疑似通信断の
操作も、この地図ベース走行フローへ統合した。
Stage Aの通信・走行実証が完了した時点の実装内容と、正本仕様(1〜9節)からの追加・具体化を記録する。
利用手順の詳細はdocs/runbooks/nmea-simulation.mdを参照。
4.1節の「responseは同じid、status、成功payloadまたは型付きerrorを持つ」を次のように具体化した。
status: "ok" + result: 正常に処理できたrequestの結果。経路送信の拒否RouteAlreadyExists/EndPointMismatch等)やset_current_positionの座標不正はここに含めるRouteSubmissionResult/SetCurrentPositionResultのstatus: "error" + error: 不正JSON、未知command、protocol version不一致、framing破損6節「同時接続は拒否または既存接続終了後に受理するなど、挙動を明文化」に対し、「拒否」を採用した。
新しい接続が来た時点で別の接続が処理中の場合、新しい接続は何もreadせず即座にcloseする(既存接続を
強制切断しない)。CLIは1操作ごとに接続・切断する実装のため、通常の利用フローで二重接続は起きない。
set_current_positionの内部実装RegisteredRouteEngine.setCurrentPosition(position)として実装した。初版(Track #67初回実装)は
既存経路の末尾へ新しい点をappendしてteleportする実装だったが、レビュー(Track #67)で「経路を持たない
現在地」という正本仕様の意図と一致しない(以前の走行可能経路が残り続ける、set_current_positionを
繰り返すたびに経路とsegmentが際限なく累積する、reset_progressが設定前の古い経路先頭へ戻ってしまう、
Running/Paused/Finished中に呼んでも走行状態が曖昧なまま残る)という指摘を受け、次のとおり作り直した。
RegisteredRouteEngineに、経路を持たない「現在地アンカー」(positionAnchor: LatLon?)を専用のsetCurrentPositionは既存のroute・progressを完全に破棄し(古い経路はpositionをこのアンカーへ設定するだけの操作にした。1点だけの経路への偽装submitInitialRouteの特殊ケース)ではなく、routeそのものを操作しない専用の状態変更である。queryEndPoint/currentPositionは、routeが空の間はこのアンカーへフォールバックする。NotStartedへ戻す。既存route自体を破棄する以上、reset_progress(RegisteredRouteEngine.reset())はこのアンカーに触れないため、set_current_positionreset_progressしても設定した地点から動かない(以前のroute先頭点へ戻ることはない。経路自体がappend_route(submitContinuation)で、このアンカーをexpectedEndPointとして送る。submitContinuationは、まだ経路へ具現化されていないアンカーがあればroute先頭点として具現化してからmaterializeAnchorAsRouteStartIfNeeded)、続きの点とのsegmentを作る。submit_initial_route(submitInitialRoute)は、アンカーが設定されている間はRouteAlreadyExistsappend_routeを使う。CLIのsubmit-appendはこの契約を前提に--expected-lat/--expected-lonを要求する(変更なし)。
Drogger Tablet側のBluetooth接続先がまだ「NMEA走行シミュレータ」になっていない間(疑似NMEA送出用の
SimulatedNmeaLinkが未生成の間)、これらの操作は既存実装同様no-opだが、TCP版ではresponseに
activeLinkPresent: booleanを含め、CLIが利用者へ状況を伝えられるようにした(AIDL版の
SimulatorIpcServiceは無言no-op)。
SimulatorTcpService(app/src/debug、android.app.Service、exported=false)がloopback:47650でSimulatorIpcServiceとは異なる考え方)。RtkForegroundService.onCreate()/onDestroy()から起動・停止する。既存createNmeaLinkFactoryとdev.drogger.tablet.service.SimulatorTcpServiceLifecycle.kt、debug/release双方に配置)。SimulatorTcpRequestHandlerはSimulationRuntime.lockでRegisteredRouteEngineアクセスを直列化し、advance()と交差させない(既存SimulatorIpcServiceと同じ方針)。SimulatorTcpServerはplain java.net.ServerSocket/Socketのみで実装し、AndroidService/Contextに依存しない(app/src/testDebugでのJVM単体テストを可能にするため)。独立CLIではなく既存pc-toolのサブコマンド(pc-tool simulator <operation>)として実装した。理由は
既存のJVM実行基盤(applicationプラグイン、installDist配布)・JSON依存(org.json:json)・
CLI引数解釈(parseFlag)をそのまま再利用でき、Main.ktへサブコマンド分岐を数行追加するだけで
済むため(独立CLIを新設した場合の重複が大きい)。:pc-toolが
:simulator-protocolへ依存を追加した(implementation(project(":simulator-protocol")))。
app-release.apkのclasses*.dexを展開し、stringsでdev/drogger/tablet/simulation・dev/drogger/simulator・SimulatorTcpを検索して0件であることを確認した(唯一ヒットするのはSimulatorTcpServiceLifecycleKtで、simulation/simulator配下への参照をprocessReleaseManifestForPackage)にSimulatorTcpService/SimulatorIpcServiceの<service>宣言が存在しないことを確認した(debug向けmanifestには両方存在)。SimulatedNmeaLinkがSimulationController.activeLinkが非nullになり、tickerがRegisteredRouteEngineを