Issue #60 / Track #69。現地を仕様探索やデバッグに使わず、PC上で「計画→疑似走行→記録→書き出し→再生・分析」を完成させてからトラクターへ持ち出すための実装手順書。
本書は実装順の提案であり、未実装部分の現行製品仕様ではない。利用者レビューで採用後、確定したデータ形式と画面挙動を
docs/specifications/へ反映する。
最初の現地試験より前に、PC、Android Emulator、debug専用NMEA走行シミュレータだけで次の閉ループを成立させる。
現地へ進む条件は「初期機能が動いた」ではなく、上記8項目、全自動テスト、debug/release build、PCでの利用者検収がすべて完了したこととする。
.dtrk v3へのナビ計画埋め込み。初期版では、clip後の候補経路が1本の連続segmentになる筆だけを自動順序の対象とする。穴、凹形状、MultiPolygon等により1候補経路が複数segmentへ分かれる場合は、計画を拒否するのではなく警告して手動次経路選択へfallbackする。圃場外の接続線を自動生成しない既存契約を維持する。
track-coreで次の責務を独立させる。
| 責務 | 入力 | 出力 |
|---|---|---|
| 筆・入口選択 | 筆一覧、登録入口、直近fix列、現在位置、手動指定 | 対象筆、入口、進入方向、選択根拠 |
| 方位決定 | 選択筆、隣接耕の角度 | 中央部だけを横切る直線ReferenceRoute |
| 固定経路幾何 | 筆、入口、Profile | 追い詰め区間、3/1/2列目の周回経路、退出位置 |
| 隣接耕幾何 | 筆、基準直線、作業幅、重ね幅制約、始終端制約 | 調整済みCandidateRoute一覧とfallback |
| 走行順 | 固定経路、隣接耕経路、入口、pattern | 工程、順序、各経路の進行方向、作業状態 |
| 案内 | 確定計画、現在位置、COG | 現在経路、横ずれ、前後進捗 |
| 永続化 | 確定計画、イベント | version付きsnapshot/event |
既存 CandidateRouteGenerator は再利用する。A-B取得で作った ReferenceRoute.Straight と、筆+方位から作る仮想的な ReferenceRoute.Straight を同じ入力型へ収束させる。A-Bコードを削除したり、平行線生成を別実装したりしない。CandidateRouteGeneratorは既にReferenceRoute.Curveも処理できるため、初期の筆+回転方式は直線だけを対象としつつ、曲線A-Bから同じ候補経路・計画modelへ合流できる既存能力を維持する。
最初に実装するpatternは、中央の隣接耕だけではなく、入口から退出までを次の一連の工程として扱う。
周回列番号は入口や走行方向によらず、圃場境界から内側へ数える。1列目は境界に最も近い外側、2列目はその内側、3列目は中央の隣接耕に最も近い内側とする。この定義をdomain、fixture、画面表示、保存snapshotで共通に使う。
導入時と仕上げ周回を逆向きにする理由は二つあり、どちらもplannerの不変条件とする。
pattern IDとversionは表示名から独立させる。
patternId = "textbook-basic-tillage"
patternVersion = 1
各routeは少なくともTILL、DEADHEAD、DOUBLE_TILLの作業種別を持つ。計画上の完了と物理的な耕耘済みは別概念とし、空走りを耕耘済み面積へ数えない。列間の物理的な旋回曲線は初期版の計画対象外だが、次の目標と必要な作業状態は明示する。
追い詰めはAPPROACHに固定された一括工程ではなく、四隅ごとに完了状態を持つ独立作業として計画する。中央の隣接耕を初期方位から90度回転した場合は、隣接耕の開始位置が入口の対角側になるため、最初の外周走行だけでは最後の一隅を処理できない。この場合は一隅を未完了のまま隣接耕へ入り、その隅へ最も近づく隣接耕の位置で、隣接耕を一時的に離脱して追い詰めだけを行い、中断位置へ戻って隣接耕を再開する。その後、3列目→1列目→2列目の仕上げ周回へ接続する。したがって、phaseがAPPROACH → CENTER_TILLAGEの一方向にしか進めない状態modelは採用しない。
方向回転の対象は中央の隣接耕だけとする。追い詰め区間および3/1/2列目の周回経路は筆polygonと入口に固定し、回転させない。周回まで回転すると最終退出条件を保証できないためである。
したがって、隣接耕を「開始位置から順に生成し、終点は列数の偶奇に任せる」方法は採用しない。選択した耕耘方位に対し、次を同時に満たす制約問題として生成する。
overlapWidthCmを基準値とし、レイヤー0の「最大自動重ね幅調整量(cm)」以内で実重ね幅をセンチ単位に調整する。増加・減少の双方を許すが、常に0 <= overlap < implementWidthを満たす。DEADHEADまたはDOUBLE_TILL actionを挿入する。これにより、開始側や最初の進行方向を後付けで反転して帳尻を合わせず、遠い側の開始条件と3列目周回への終端条件を同じ計画計算で保証する。利用者による方向回転のたびに全制約を再計算する。
入口は一つの取得方法へ限定しない。優先順位は次のとおりとする。
現在位置が圃場外、進入直後、圃場内のいずれでも計画作成できる。計画snapshotには入口ID、境界座標、進入方位、取得元REGISTERED / RECENT_CROSSING / MANUAL_BOUNDARYを保存する。登録入口は筆polygon UUID、名称、既定flag、作成・更新時刻を持つ。
開始前previewでは、追い詰め、移動、隣接耕、3/1/2列目周回、退出を含む調整後の全計画経路を表示する。色だけでなく線種、太さ、番号、矢印、TILL / DEADHEAD / DOUBLE_TILLの文言で区別する。
数値として、基準重ね幅、実重ね幅、基準からの調整量、実ピッチ、隣接耕列数、偶奇、予想到達位置、fallbackの有無と種類を表示する。計画が成立しない場合は開始操作を無効化し、どの制約が成立しないかを示す。
既存 .dtrk v3はGNSS実走点列として変更しない。記録中は1作業につき1ディレクトリを使い、終了後に単一の .dwork ZIPへ確定する。
track/work-sessions/active/<session-uuid>/
├── manifest.json
├── navigation-plan.json
├── navigation-events.jsonl
└── trajectory.dtrk
track/work-sessions/completed/<開始日時>_<session-uuid>.dwork
役割は次のとおり。
manifest.json: schemaVersion、sessionId、作成/更新/終了時刻、アプリversion、状態、関連ファイル名。navigation-plan.json: pattern ID/version、筆polygon snapshot、Profile snapshot、基準方位、生成済み経路、走行順、warnings。確定後は不変。navigation-events.jsonl: plan confirmed、guidance started/suspended/resumed、target changed、route skipped/completed/reopened、guidance stopped等。1イベント1行で追記し、毎回flushする。trajectory.dtrk: 既存v3 writerが記録する実走軌跡。作業セッション内では1ファイルを基本とする。.dwork: 終了後の上記ファイルを格納したZIP。ZIP entry名、JSON schema、必須entryをversion管理する。navigation-plan.json と manifest.json は一時ファイルへ完全書込み・flush後、同一ディレクトリ内でrenameして置換する。navigation-events.jsonl は末尾の不完全な1行を読み込み時に無視する。.dtrk は既存の末尾不完全レコード切捨てとresumeを利用する。.dwork.tmp を作り、全entryを書いて検査した後に .dwork へrenameする。途中失敗時はactive directoryを残し再試行可能にする。.dwork は既存 .dtrk 一覧と別種類の記録である。移行期間中は従来の単独 .dtrk も読み続け、作業セッションを伴う記録だけを .dwork として新一覧へ出す。
.dworkと従来の.dtrkは別一覧とし、.dwork削除時は内包するplan、events、trajectoryを一緒に削除する。各手順をIssue #60配下の独立Trackまたは、少なくとも独立した作業記録境界として扱う。後続手順のコードを前倒しで混ぜない。各境界で対象テストに加え全体回帰とrelease buildを通す。
docs/specifications/p6-ui-spec.mdへ全体計画入口、回転編集、基本パターン、レイヤー0設定、作業セッション、PC完了gateを追記する。docs/specifications/p6-trajectory-file-format.mdへ、.dtrk v3を維持し .dwork 内の実走entryとして使う判断を追記する。docs/specifications/p6-work-session-format.md を作り、JSON/JSONL/ZIPの正確なschema、enum、時刻、座標順、互換方針を定義する。完了条件: 文書間でA-B必須、複数pattern一括実装、早期現地試験という旧記述が新方針と矛盾しない。
対象: track-core。
TillagePlan、TillagePhase、RouteAction、EntranceSnapshot、OverlapAdjustment、warning/fallback型を定義する。RouteActionは工程順、geometry、進行方向、TILL / DEADHEAD / DOUBLE_TILL、追い詰め・隣接耕・周回列番号・退出の役割を保持する。TillagePlanningSettingsに最大自動重ね幅調整量と既定fallbackを持たせ、手順0で確定した範囲をdomain validationとして実装する。テスト:
対象: track-core、必要最小限の pc-tool CLI。
FieldGeometryOpsはinternalのまま保ち、JTS型を漏らさない公開facade FieldPlanningGeometryを追加する。EntranceSnapshotへ正規化する。直近fix bufferはメモリ上だけに保持する。FieldAxisReferenceFactoryで中央隣接耕の方位だけを生成する。TillagePlanへ構成する。pc-tool guidance-planで筆、入口、Profile、heading、最大調整量、fallbackを入力し、plan JSONとGeoJSONを出力できるようにする。テスト:
対象: track-core、:app、pc-tool、debug simulator。実機、Drogger、作業セッション保存はまだ使わない。
Layer0SettingsScreenへ「最大自動重ね幅調整量(cm)」と「成立しない場合の既定fallback(空走り/二重耕耘)」を追加する。手順0で確定した初期値・範囲・刻みを使い、専用store interfaceとSharedPreferences実装で永続化する。既存のTrajectoryTurnDetectionSettingsStoreと同じく、UIから直接SharedPreferencesへ依存させない。TillagePlanningSettingsへmappingし、Tablet plannerとPC CLIが同じdomain validationを通るようにする。未保存時だけ確定済み初期値を使い、範囲外・破損値は安全な初期値へ正規化して診断ログへ残す。WorkMenuから「圃場全体を計画」を開始し、筆と入口候補を確認する。完了条件:
:track-core:test、:app:testDebugUnitTest、:pc-tool:test、:pc-tool:testSimulatorUiTs、:app:assembleDebug、:app:assembleReleaseがgreenで、release APKにdebug simulatorが混入しない。この検収点を、次に着手する実装単位とする。計画の永続化、案内、疑似完走、.dworkは、画面上の計画が利用者確認を通ってから進める。
対象: track-core。
apply falseで追加し、track-coreへ適用する。kotlinx-serialization-jsonのversionは実装開始時にKotlin 2.2.10との互換を公式情報で確認して固定する。track-coreのworksession packageへ WorkSessionManifest、NavigationPlanSnapshot、NavigationEvent、schema versionを定義する。kotlinx.serializationでtrack-core内に一つだけ実装し、Androidのorg.jsonにもPC専用org.jsonにも依存させない。:appとpc-toolが同じcodecを呼ぶ。WorkSessionReaderはignoreUnknownKeys=trueで未知の将来fieldを無視し、未知のschema majorはcodecの前段で明示エラーにする。encode時のfield順と浮動小数点表現をfixture testで固定する。track-core/src/test/resources/work-sessions/ に置く。テスト:
この手順ではAndroid filesystem、ZIP、UIを触らない。
対象: :appのService層、track-core writer。
WorkSessionStore interfaceとfilesystem実装を作る。RtkForegroundServiceが所有する WorkSessionController を追加する。Composeの remember を永続状態の正にしない。Readyでのpreview中は何も記録しない。「ナビ開始」操作で、確定geometryを凍結し、active directory、manifest、plan snapshot、event writer、.dtrk writerを原子的に開始する。全開始処理に成功した場合だけNavigatingへ遷移し、途中失敗時はReadyに留めて軌跡writerだけが動く状態を残さない。SwitchableTrajectoryRecorder と DtrkFileWriter を再利用し、通常の単独記録を壊さない。.dwork.tmpを作成・検査し、completedへrenameする。既存の「記録を再開」「新しい記録を開始」と作業セッションの関係を明示する。初期推奨は、ナビ作業中の主記録を作業セッションが所有し、従来ボタンから同じwriterを二重開始できないよう状態機械で排他にする。
plan confirmedとguidance startedは意味の異なる2イベントとして保存するが、利用者操作は同じ「ナビ開始」1回であり、同じ開始transactionからこの順で発生させる。計画確定だけを行う独立操作は設けない。
テスト:
.dtrk末尾途中からの復旧。対象: track-core reducer、:app mapping。
既存 GuidancePlanState を無理にBooleanで拡張せず、入口を含む排他的状態へ再設計する。例:
Idle
→ FieldSelecting
→ DirectionEditing
→ PatternPreview
→ Ready
→ Navigating
→ Completed
Readyはplannerが成立し開始前previewを確認できる状態であり、計画確定済み・記録中という意味ではない。「ナビ開始」eventだけが、計画geometryの確定、作業セッションと軌跡記録の開始、Navigatingへの遷移を一括して要求する。副作用側の開始が失敗した場合、reducerへ成功eventを返さずReadyを維持する。
A-B取得は FieldSelecting とは別のreference作成入口として既存reducerを再利用し、最終的に同じ PatternPreview へ合流させる。
Suspendedをこの作業段階sealed classへ追加しない。一時的な案内可否は既存のGuidanceAvailability.Available / GuidanceAvailability.Suspended(reason)をナビ全体へ拡張して表し、Navigatingと直交させる。Suspended中も作業段階、plan、現在targetをNavigatingのまま保持し、横ずれ表示と自動完了・自動target遷移だけを止める。既存型のコメントと導出関数は、現在の「A/B取得可否」専用という説明から、計画操作・案内中を含むナビゲーション全体の可否へ更新する。新しい同名Suspended状態やBooleanを重ねて作らない。
イベントには筆・入口確認、隣接耕角度変更、重ね幅再計算、fallback確認、plan確定、案内開始、target変更、skip、complete、reopen、終了を持たせる。reducerは副作用を持たず、accepted transitionから保存イベントを作るmappingも純粋関数でテストする。
テスト:
対象: MapScreen、OfflineMapStyle、WorkDock、WorkMenu、共通presentation。
PatternPreviewは初期版では基本耕耘1種類でも、将来のpattern選択を追加できるmodelにする。未実装patternを無効カードとして並べない。800×1340で、通常、長い筆名、warning、通信断、ダイアログ、航空写真背景を確認する。Compose Previewだけで完了にしない。
対象: track-core guidance、:app Service/presentation、debug simulator。
ProfileSnapshotからoffsetForwardCm/offsetLateralCmを取り出し、現在のアンテナ位置と信頼できる進行方位からライブ作業機中心を求める専用の純粋関数(仮称ImplementPositionTransform)をtrack-coreへ追加する。座標移動の数式は既存GeoMathと共用する。点列履歴から旋回区間を除外して描画用segment IDを振り直すTrajectoryOffsetTransform.apply(List<TrackPoint>, Int, Int, TrajectoryTurnDetectionSettings)は責務が異なるため、ライブ案内へ直接流用しない。GuidanceAvailability がSuspendedなら横ずれを無効化し、計画・現在targetを保持し、自動完了/遷移を止める。.dtrk が同じ時刻軸で記録されることを結合テストする。テスト:
.dwork一覧・書き出し・削除対象: 保存記録UI、SAF export。
WorkSessionSummaryLoaderを追加し、.dworkのmanifestとplanから一覧情報を読む。WorkDestination 骨格を維持する。.dworkを選択して単一ファイルとしてSAFへ書き出す。成功後だけ exported/ へ移動する既存契約を踏襲する。.dtrk 一覧・再生・書き出しを壊さず、段階的に「軌跡記録」と「ナビ作業記録」を区別する。テスト:
.dtrk 一覧と再生の回帰。対象: pc-tool Kotlin server/CLIと既存Web UI。
解析ロジックは track-core に置き、TypeScriptへ再実装しない。TypeScriptは表示・操作だけを担う。
pc-tool work-session inspect <file.dwork> でmanifest、plan、events、trajectoryを検査する。pc-tool work-session analyze <file.dwork> で目標経路への投影と統計を生成する。.dwork 読込みを追加し、筆、計画線、実走、時刻marker、eventを同じ地図へ表示する。最小統計:
テスト:
.dwork fixtureで期待統計を厳密比較。実際の現地候補筆を使い、次をPC上で通す。
.dworkを書き出し、PC Web UIへ読み込む。総合gate:
cd apps/android-tablet
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew \
:track-core:test \
:pc-tool:test \
:pc-tool:testSimulatorUiTs \
:app:testDebugUnitTest \
test \
:app:assembleDebug \
:app:assembleRelease \
:pc-tool:installDist \
--max-workers=1
加えてrelease APKのmanifest/dexにdebug simulatorのTCP command、service、class名が含まれないことを既存runbookどおり検査する。
以下がすべて満たされるまでDroggerをトラクターへ装着しない。
現地でのみ確認する項目は、実GNSS精度、アンテナ/作業機offset、圃場境界との差、旋回余裕、振動・日光・手袋での操作性に限定する。現地で新しいpatternを実装・調整しない。
CandidateRouteGenerator、実筆fixtureはそのまま採用する。remember 保持と Confirmed までの状態は作業セッション所有へ移す。track_update_parent(mode='preview') で確認してから行う。.dtrk v4へ可変長planを詰め込まない。各手順で以下を行う。
track_record へ残す。work_record へ残す。p6-ui-spec.md、p6-work-session-format.md、必要に応じて p6-pc-simulator-spec.md へ反映する。track_check(parent_issue_id=60) と全体gateを実行する。