この文書の位置づけ(Issue #132で変更): DTRK記録フォーマットの正本は
経路記録データファイルフォーマット(.dtrk)である。
本書はV1〜V4の策定経緯・設計判断、schema 1の計画JSON詳細、失敗計画JSON、Stage 0判断改訂を残す
詳細・履歴資料とする。wire契約の記述が正本と食い違う場合は正本を優先し、Issue #132で差分を解消する。
正本が本書の詳細節を明示的に参照している項目は、その参照先を補足定義として読む。
Issue #38(P6)Track #48(P6-10、軌跡データの保存・一覧・再生・端末外への吸い出し)で採用する、軌跡記録の永続化ファイル形式の仕様。P6-9で実装したJSON Lines形式(track/trajectory.jsonl)を置き換える。
P6-9では1行1JSON({"atMs":...,"lat":...,"lon":...,"segmentId":...})を単一ファイルへ追記する方式を採用したが、P6-10で以下の要件が加わり形式を見直した。
V1〜V4に共通する設計方針・ファイル配置・共通処理をまとめる。バージョンごとに異なるヘッダ・レコード構造は各フォーマットの章を参照する。V4以降の任意debug schemaは、ファイル本体のversionではなくheaderLength・recordLengthとdebug format ID/versionで判別する。
recordLengthをヘッダへ記録する。同じファイル内では全レコードを同じ長さに保つため、「データ部のbyte数をrecordLengthで割った余り」を切り捨てるだけで末尾破損に対応できる。segmentIdはレコードごとに繰り返さず、ヘッダに1回だけ記録する。flush()する。圧縮を避けるのは、書き込み途中の破損が後続の全レコードへ波及するのを防ぐため。これは追記される本体列だけの制約で、記録開始前に一括で書き切って凍結するdebugHeaderPayloadや.dtrk外の.plan.jsonは対象外(V4章参照)。セグメント確定後のファイル全体圧縮「compress-on-finalize」は将来検討(このドキュメントでは仕様化しない)。<外部ストレージ>/track/records/{プロファイル名(サニタイズ済み)}_{yyyyMMdd-HHmmss}.dtrk
Issue #96のdebug .dtrkと失敗・非実走.plan.jsonは<外部ストレージ>/track/records/debug/へ置く。書き出し済みdebug成果物はその配下のexported/へ移す。
FileDiagnosticRecorderが診断セッションディレクトリ名に使っている書式(yyyyMMdd-HHmmss)と統一する。/ \ : * ? " < > |等)を除去・置換してから使う。segmentId)を作る。ヘッダの先頭6byte(magic+version)は、将来バージョンがいくつ増えてもオフセット・サイズ・意味を変更しない。ここだけを読めば、他のどのフィールドも解釈せずに「このファイルがDTRK形式かどうか」「続くバイト列をどのバージョンの定義で読むべきか」を判定できる必要があるため(version自体をどのレイアウトで読むかが分からなければ、バージョン判定ができず堂々巡りになる)。全フィールドはbig-endian(既存のSessionLogWriter/SessionLogReaderと統一)。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4byte | ASCII | magic | "DTRK"(0x44 0x54 0x52 0x4B)。他形式ファイルの誤読を防ぐ識別子。 |
| 4 | 2byte | UInt16 | version | フォーマットバージョン。この値を読んでから、対応するバージョン(「V1フォーマット」「V2フォーマット」)の章の定義に従って残りを解釈する。 |
オフセット6以降(reserved/headerLengthを含むヘッダの残り・レコード全体)は、各バージョンの章で個別に定義する。位置・サイズが変わらないことが保証されるのはoffset 0-5だけで、offset 6以降の意味はversionによって変わりうる(実際、offset 6-7はV1ではreserved、V2以降ではheaderLengthと意味が変わる。詳細は各章を参照)。V1〜V3で共通する部分も各versionが過去レイアウトを流用した結果であり、恒久的な保証ではない。V4では、以後の互換拡張で維持する「base header」と「base record prefix」を改めて定義する。
fileSize >= 6を確認し、offset 0〜3のmagicを検証する。不一致はDTRKとして扱わない。36/10、V2は40/13、V3は42/13で固定する。V1のoffset 6〜7を長さに使わない。V4だけheader内のheaderLength/recordLengthを実境界に使う。fileSize < headerLengthは不正とする。(fileSize - headerLength) % recordLengthの余りは、電源断等による末尾の不完全record/frameとして切り捨てる。V1・V2とも、レコード内のdLat/dLonはInt24で表現する。DataOutputStream/DataInputStreamに24bit整数の読み書きは無いため、以下の変換で扱う。
0xFFで埋める)。表現範囲: 符号付き24bit = ±8,388,607。1e-7度(≒1.11cm)を掛けると 約±93km(初期値からの距離として)。
(fileSize - headerLength) % recordLengthの余りを末尾の不完全なレコード/frameとみなし自動的に切り捨てる(headerLength/recordLengthの求め方はversionごとに手順3を参照)。SessionLogReaderが可変長形式で行っている「壊れた末尾を切り捨てそれまでは有効とする」方針と同じ考え方。P6-9→P6-10で実装済み、実機記録実績あり。
先頭6byte(オフセット0-5)は「基本フォーマット」で定義した自己記述領域(magic/version)で、V1ではversionが1になる。オフセット6以降がV1固有のフィールド(reservedは未使用領域で、V2のheaderLengthとは異なる)。全フィールドはbig-endian(既存のSessionLogWriter/SessionLogReaderと統一)。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4byte | ASCII | magic | "DTRK"(0x44 0x54 0x52 0x4B)。他形式ファイルの誤読を防ぐ識別子。 |
| 4 | 2byte | UInt16 | version | フォーマットバージョン。V1では1(V2では2になる。詳細は「V2フォーマット」章を参照)。 |
| 6 | 2byte | UInt16 | reserved | 0固定(未使用)。V2で同じ位置に導入されるheaderLengthとは無関係。実機記録済みのV1ファイルはこの位置に0が書かれているため、V1ファイルの読み込みではこの値を長さとして解釈してはならない(後述のデコード方法を参照)。 |
| 8 | 4byte | Int32 | segmentId | このファイルが属するセグメントID。ファイル内で共通。 |
| 12 | 8byte | Int64 | initialAtMs | セッション開始点(このファイルの最初のレコード)の絶対epoch ms。 |
| 20 | 8byte | Float64 (IEEE754) | initialLat | セッション開始点の絶対緯度(度)。量子化なしのフル精度。 |
| 28 | 8byte | Float64 (IEEE754) | initialLon | セッション開始点の絶対経度(度)。量子化なしのフル精度。 |
ヘッダ合計: 4+2+2+4+8+8+8 = 36byte。
ファイルの最初の点(セッション開始点そのもの)はヘッダのinitialAtMs/initialLat/initialLonが担うため、レコード列は2点目以降を格納する。
| オフセット(レコード内) | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4byte | Int32 | elapsedMs | initialAtMsからの経過ms(常に正の値、直前のレコードからではなく常にヘッダ基準)。 |
| 4 | 3byte | Int24(符号付き) | dLat | initialLatからの緯度オフセット(1e-7度単位)。 |
| 7 | 3byte | Int24(符号付き) | dLon | initialLonからの経度オフセット(1e-7度単位)。 |
レコード合計: 4+3+3 = 10byte。
セッション開始点(そのファイルの最初の点)をp0 = (atMs0, lat0, lon0)として:
ヘッダ書き込み:
magic = "DTRK"
version = 1
reserved = 0
segmentId = <現在のセグメントID>
initialAtMs = atMs0
initialLat = lat0
initialLon = lon0
2点目以降の各点p = (atMs, lat, lon)に対し、レコードを書き込む:
elapsedMs = atMs - atMs0 // Int32
dLat = round((lat - lat0) / 1e-7) // Int24(符号付き)
dLon = round((lon - lon0) / 1e-7) // Int24(符号付き)
各点を受信するたびに1レコードを追記し、即座にflush()する(クラッシュ耐性、P6-9からの既存方針を踏襲)。
範囲超過時の扱い: dLat/dLonが±8,388,607を超える場合(セッション開始点から93kmを超えて移動した場合)、そのまま整数へ詰めるとオーバーフロー(ラップアラウンド)し、無音のまま位置が大きく狂ったレコードになる。これを避けるため、範囲の最大/最小値へクランプして記録する(位置に誤差は生じるが、無音の暴走地点にはならない)。同様にelapsedMsが31bit符号付き範囲(約24.8日)を超える場合もクランプする(通常の1セグメントの継続時間ではまず起こらない)。
ヘッダを読み、magic/versionを検証する。一致しなければこのファイルは扱わない(空リストを返す)。
segmentId, initialAtMs, initialLat, initialLonを読む(reservedは無視する)。
points = [ (initialAtMs, initialLat, initialLon, segmentId) ] // ファイル先頭の1点
残りバイト数 = ファイルサイズ - 36 // V1のヘッダ長は固定36。実機記録済みファイルのreserved領域は`0`のため、ここを長さとして読んではならない(V2との違いは「V1との後方互換」節を参照)
レコード数 N = floor(残りバイト数 / 10) // 末尾の不完全なレコード(破損・書き込み途中断)は自動的に切り捨てられる
N回繰り返し、各レコードについて:
elapsedMs, dLat, dLon を読む
atMs = initialAtMs + elapsedMs
lat = initialLat + dLat * 1e-7
lon = initialLon + dLon * 1e-7
points に (atMs, lat, lon, segmentId) を追加
実測GGA受信レート(P6-9で確認、約9〜10Hz)を前提に、1日の稼働時間ごとの概算:
| 稼働時間 | 点数(9.5Hz換算) | サイズ(10byte/点 + ヘッダ36byte) |
|---|---|---|
| 1時間 | 約34,200点 | 約342KB |
| 6時間 | 約205,200点 | 約1.96MB |
| 8時間 | 約273,600点 | 約2.61MB |
JSON形式(P6-9、1点あたり約90〜100byte)比で約9〜10倍の削減。
Track #49(P6-11)で、走行軌跡へアンテナ―作業機オフセットを反映するために策定した拡張。P6 UI仕様の「アンテナ―作業機オフセット」節も参照。
以下をすべて実装済み(testDebugUnitTest/assembleReleaseとも成功)。
NmeaParser.decodeRmcがCOG(courseDegrees)・対地速度(speedKnots)をデコードし、FixRepositoryがノット→m/s変換の上でTrackPoint.courseDeg/speedMpsへ付与する。DtrkFormat/DtrkFileWriter/DtrkFileReaderがV1・V2両方を読み書きする(新規記録は常にV2、既存V1ファイルはversionで分岐して読み続ける)。Profileにアンテナ―作業機オフセット(offsetForwardCm/offsetLateralCm)を追加し、レイヤー1のプロファイル編集画面から設定できる。記録開始時点の値がV2ヘッダへ書き込まれる。TrajectoryTurnDetectionSettings(COG信頼最低速度・旋回ヨーレート/速度閾値)を追加し、設定画面から調整できる。TrajectoryOffsetTransform(描画専用の変換ロジック、trackパッケージ)がメカニズム1(方位の穴埋め)・メカニズム2(旋回区間の除外)を実装する。2026-09-04の利用者判断(#124)により、中心線はTrajectoryOffsetTransform.centerline(位置調整なし=アンテナ位置)、耕耘あと帯はTrajectoryOffsetTransform.apply(記録時の前後・左右位置調整で作業機中心へ移す)を使い分ける。旋回区間除外とsegment/file境界は中心線・帯で共通。測位系画面(ライブ)・再生画面・PC検証ツールで意味を統一する。未実装・未検証のまま残るもの(Track #49で明記した既知の残課題、このサイクルでも解消していない):
P6-9〜P6-10のV1形式は、軌跡点(アンテナ位置)の座標だけを持ち、方位(COG)を持たない。トラクターの実際の耕耘位置(ロータリー中心)はアンテナからオフセットしているため、耕耘あと帯を作る際にはオフセット変換が必要であり、その方向を求めるために各点の方位が要る。アンテナ生位置の記録方式は維持し、方位(COG)・速度をレコードへ追加する。2026-09-04の利用者判断により、表示上の中心線はアンテナ生位置を示し、オフセット変換は耕耘あと帯だけへ適用する。記録データ自体へオフセットを焼き込まないため、表示規則を変更しても記録済みデータを再収集する必要はない。
先頭6byte(オフセット0-5)は「基本フォーマット」の自己記述領域(magic/version)で、versionが2になる。オフセット6-7は、V1ではreserved(未使用)だった位置に、V2で新たにheaderLength(ヘッダ全体のbyte数、値40)を導入する。オフセット8-35(segmentId/initialAtMs/initialLat/initialLon)はV1のフィールド定義をそのまま流用した(V2固有の設計判断であり、自己記述領域のような「変更しない」保証があるわけではない)。末尾に以下を追加する。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0〜5 | 6byte | (基本フォーマット参照) | magic/version | 自己記述領域。versionは2になる |
| 6 | 2byte | UInt16 | headerLength | ヘッダ全体のbyte数。V2では40固定。V1でreserved(常に0)だった位置に、V2で新たに意味を持たせたフィールド。 |
| 8〜35 | 28byte | (V1のレイアウトを流用) | segmentId/initialAtMs/initialLat/initialLon | V1の定義をそのまま流用(V2固有の設計判断) |
| 36 | 2byte | Int16 | offsetForwardCm | このセグメント記録開始時点の、プロファイルの前後オフセット(cm) |
| 38 | 2byte | Int16 | offsetLateralCm | 同、左右オフセット(cm) |
ヘッダ合計: 6 + 2 + 28 + 2 + 2 = 40byte。
headerLengthを設けた理由: ヘッダとレコード列(データ部)の境界を、バージョンごとのヘッダサイズを実装側にハードコードせずに判定できるようにするため。V1にはこのフィールドが無い(オフセット6-7は常に0のreserved)ため、この恩恵を受けられるのはV2以降で新たに書かれたファイルのみ。V1ファイルの読み込みでは、version==1と判定した時点でヘッダ長を固定値36として扱う(「V1との後方互換」節を参照)。ただしTrack #121開始時点のV2/V3 reader実装はこの意図と異なり、読み取ったheaderLengthを境界計算へ使わずversion別定数を使っている。V4実装ではこの乖離を解消し、ファイル内の長さを実際に使う。
オフセットはセグメント(ファイル)ごとに1回だけ記録する(監査目的: 後からプロファイルのオフセット設定を変更しても、既存ファイルがどの値で記録されたかを追跡できる)。ヘッダが担うセッション開始点(1点目)には、COG・速度を持たせない。そのためだけに専用フィールドを設けるより、他の「COGが無効な点」と同じフォールバック経路(後述)で扱う方が単純であり、影響も数万点中の1点・約100ms未満に限られるため。
V1レコード(10byte、elapsedMs/dLat/dLon)の定義をそのまま流用し(V2固有の設計判断)、末尾に追加する。
| オフセット(レコード内) | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0〜9 | 10byte | (V1のレイアウトを流用) | elapsedMs/dLat/dLon | 変更なし |
| 10 | 2byte | UInt16 | cogDeciDeg | この点のCOG(進行方向)。0.1°単位(0〜3599)。0xFFFF=無効(受信機がRMCのCOGフィールドを空欄で送ってきた場合、NMEA仕様上あり得る) |
| 12 | 1byte | UInt8 | speedByte | この点の対地速度。0.05m/s刻み(0〜254 = 0〜12.7m/s)。0xFF(255)=無効 |
レコード合計: 10 + 2 + 1 = 13byte。
0xFFFF/0xFFを「無効」のセンチネル値にするのは、RMCのCOG・速度フィールド自体がNMEA仕様上「値が無ければ空欄」になり得るためで、これにより描画側で「この点はCOG/速度が無効」と明確に判別できる。単純な「低速・停止=継続表示/旋回=非表示」という二分法ではなく、2つの別レイヤーの処理として整理する。詳細な検討経緯はP6 UI仕様の「アンテナ―作業機オフセット」節を参照。
cogDeciDegが0xFFFF)、または速度(speedByte)が閾値未満で瞬間的なCOG値の信頼性が低いと判断される間は、オフセット変換に使う「向き」として直前の有効な方位を保持する。これはオフセットの計算方向だけの話であり、その点を線として描画するかどうかとは別問題。分岐は「読み取りの共通手順」に従う(version==1は36/10、version==2は40/13)。version==1ではCOG/速度/オフセットを「情報なし」(アンテナ生位置のみ)として扱う。これにより既に実機で記録済みのV1ファイルも引き続き読める。
version==1のオフセット6-7のreservedは常に0(実機記録済みファイルの実際の値)であり長さの情報を持たないため、ヘッダ長は固定値36として扱う。V2でheaderLengthを導入した仕様上の目的はV2以降がこの値を実データ開始offsetとして使うことだったが、Track #121開始時点の実装はV2=40、V3=42のversion別定数を使っており、既存ファイルはその実装互換を維持する。ファイル内の長さを拡張境界として実利用する契約はV4から必須化する。
6時間稼働(実測レート約9.5Hz換算、約20.5万点)の試算で、V1の約1.96MBに対しV2は約2.67MB(約+36%)。書き出し・保存の運用上は無視できる差と判断している。
耕耘あと(作業機幅を反映した帯)表示のため、作業機(ロータリー)の全体幅(implementWidthCm)を追加した拡張。「PCで先に耕耘あと表示を確立し、同じロジックをタブレットのリアルタイム描画にそのまま反映する」という開発方針(Track #54)の一環で、まず作業機幅をProfile・記録ファイルの両方に持たせる。
V2のヘッダ(40byte、headerLengthは40)の末尾にimplementWidthCm(2byte)を追加する。それに伴いheaderLengthは42になる。オフセット0-35(magic/version/headerLength/segmentId/initialAtMs/initialLat/initialLon)・36-39(offsetForwardCm/offsetLateralCm)はV2の定義をそのまま流用する。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0〜5 | 6byte | (基本フォーマット参照) | magic/version | 自己記述領域。versionは3になる |
| 6 | 2byte | UInt16 | headerLength | ヘッダ全体のbyte数。V3では42固定 |
| 8〜35 | 28byte | (V1のレイアウトを流用) | segmentId/initialAtMs/initialLat/initialLon | V1の定義をそのまま流用 |
| 36 | 2byte | Int16 | offsetForwardCm | V2から変更なし |
| 38 | 2byte | Int16 | offsetLateralCm | V2から変更なし |
| 40 | 2byte | Int16 | implementWidthCm | このセグメント記録開始時点の、プロファイルの作業機(ロータリー)全体幅(cm)。負値は想定しないが、型はV2のオフセット2フィールドと揃えてInt16にした |
ヘッダ合計: 6 + 2 + 28 + 2 + 2 + 2 = 42byte。
V2導入時に意図したheaderLengthにより、仕様上はヘッダサイズを自己記述できる。ただしTrack #121開始時点のreader実装はV2/V3ともversion別定数を境界に使っているため、既存V3を同じversionのまま伸ばしてはならない。この実装済みreaderとの互換境界を明確にするため、任意拡張は後述のV4基底形式から開始する。
作業機幅はセグメント(ファイル)ごとに1回、ヘッダにのみ記録する(点ごとに変化しない値のため)。レコード構造はV2のレイアウト(elapsedMs/dLat/dLon/cogDeciDeg/speedByte、13byte)をそのまま流用する。
Track #54時点での初弾スコープ:
implementWidthCm/2ずつ広げた矩形帯として連結する(自己交差・オーバーラップの処理は初弾では行わない)。オフセットが0でなければ中心線と帯の中央は一致しない。track-core)に実装し、PC向けCLI(pc-tool)と:app(タブレット)の両方から同一コードとして参照する。判定ロジックをPC/タブレットで別言語に翻訳することはしない(Track #54の議論を参照)。分岐は「読み取りの共通手順」に従う(version==1は36/10、version==2は40/13、version==3は42/13)。version<3では作業機幅を「情報なし」(version==1はCOG/速度/オフセットも)として扱う。V1〜V3のヘッダ長・レコード長は既存readerとの互換のためversion別定数、V4からはファイル内のheaderLength・recordLengthを実境界に使う。
V4は、現地ナビ検証で必要になる計画・測位品質・ナビ状態等を.dtrkへ追加しても、そのデバッグ拡張を知らないV4対応reader/writerが通常の軌跡情報を読み書きできることを目的とする。
recordLengthとoptional debug headerを導入するための一度だけの構造変更である。versionを上げない。versionを上げるのは、base header、base record prefix、長さ・skip規則など、未知debugを読み飛ばすだけでは通常軌跡を維持できない非互換変更に限る。debugFormatIdとdebugFormatVersionを持つ。本体versionとdebug schema versionを混同しない。ここでいう「未知のdebug schemaと通常機能を両立する」とは、debug値を解釈・生成できるという意味ではない。readerは通常軌跡を読み未知領域をskipでき、writerはdebug schemaを知らなくても46/13の通常V4を新規作成できることを意味する。再起動後に既存fileへ追記する運用は行わない。
V4はheaderLengthをUInt32とし、未圧縮のdebug JSONを収容する。この46byte layoutは、V4策定時
(Issue #96 / Track #121、旧reader未配布の段階)に初版として確定して以降、据え置いている。
V4記録は2026-09-06に実機取得実績がある。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4byte | ASCII | magic | DTRK |
| 4 | 2byte | UInt16 | version | 4 |
| 6 | 4byte | UInt32 | headerLength | base headerとoptional debug headerを含むヘッダ全体長。最小46 |
| 10 | 4byte | Int32 | segmentId | V3までと同じ |
| 14 | 8byte | Int64 | initialAtMs | 全レコード差分の基準epoch ms |
| 22 | 8byte | Float64 | initialLat | 全レコード差分の基準緯度 |
| 30 | 8byte | Float64 | initialLon | 全レコード差分の基準経度 |
| 38 | 2byte | Int16 | offsetForwardCm | 記録開始時Profile snapshot |
| 40 | 2byte | Int16 | offsetLateralCm | 記録開始時Profile snapshot |
| 42 | 2byte | Int16 | implementWidthCm | 記録開始時Profile snapshot |
| 44 | 2byte | UInt16 | recordLength | 1frame全体のbyte数。最小13 |
debugなしのV4ファイルはheaderLength=46、recordLength=13となる。readerはUInt32値をホストの符号付き整数へ変換・確保する前にheaderLength <= fileSizeを検証する。
well-formedなV4は、46 <= headerLength <= fileSize、13 <= recordLength <= 65535を満たす。headerLengthが47〜49なら不正、headerLength > 46ならheaderLength >= 50かつdebugFormatId != 0、recordLength > 13ならheaderLength >= 50でなければならない。不正値からの部分復旧範囲はreader実装の裁量とする。
デバッグ情報が無いファイルはheaderLength=46とし、offset 46以降を持たない。デバッグ情報がある場合だけ、offset 46から次のdebug headerを置く。
| オフセット | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 46 | 2byte | UInt16 | debugFormatId |
debug schemaの種別。0は予約済み |
| 48 | 2byte | UInt16 | debugFormatVersion |
このdebug schemaのversion |
| 50 | 可変 | byte列 | debugHeaderPayload |
headerLength - 50byte。schema固有metadata |
debugHeaderPayloadの長さはheaderLength - 50で一意に決まるため、別のheaderPayloadLengthは持たない。同様に、各測位レコード末尾のdebug suffix長はrecordLength - 13で一意に決まるため、別のrecordPayloadLengthは持たない。
debugなし:
header: [base header 46]
record: [base record 13]
debugあり:
header: [base header 46][debugFormatId 2][debugFormatVersion 2][debugHeaderPayload ...]
record: [base record 13][debugRecordSuffix ...]
recordLength > 13のファイルは、suffixのschemaを識別できるoptional debug headerを必須とする。未知のdebugFormatIdまたはdebugFormatVersionを読む実装は、offset 46からheaderLengthまでと、各frameの既知部分以外をskipする。
offset headerLengthからrecordLength刻みで固定長frameを並べる。先頭Int32(elapsedMsの位置)が0以上なら通常測位frame、負値なら非測位frameで、その意味とrecordLength内の内部レイアウトはdebug schemaが定義する。V4 readerは負値frameを軌跡点にせず、schemaを解釈できなければframe全体をskipする。通常測位frameの先頭13byteはV2/V3と同じ値表現を使う。
| オフセット(レコード内) | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4byte | Int32 | elapsedMs | initialAtMsからの経過ms。通常測位は0以上 |
| 4 | 3byte | Int24 | dLat | initialLatからの差分、1e-7度単位 |
| 7 | 3byte | Int24 | dLon | initialLonからの差分、1e-7度単位 |
| 10 | 2byte | UInt16 | cogDeciDeg | 0.1度単位、0xFFFF=無効 |
| 12 | 1byte | UInt8 | speedByte | 0.05m/s単位、0xFF=無効 |
| 13 | 可変 | byte列 | debugRecordSuffix | debug schema固有。recordLength - 13byte |
V4ではanchorを「ヘッダだけに存在する暗黙の1点」とせず、最初の測位点も1件目のframeとして書く。通常はelapsedMs=0、dLat=0、dLon=0となる。V4の点数は通常測位frame数でありevent frameを含めない。V1〜V3 readerは従来どおりヘッダanchorを1点目として復元する。
デバッグ情報が無いファイルはrecordLength=13であり、通常レコードへ余分なbyteを一切追加しない。debug schemaが測位点ごとの情報を持つ場合だけrecordLength > 13とし、suffixを追加する。
全schema共通のextensionStateは設けない。debug suffix全byteが0のときを必ずNOT_RECORDEDとして予約し、schema固有の実データfieldまたはpresence bitmapで有効値と区別する。
有効なsuffixが偶然全byte 0になり得るschemaは、そのまま登録してはならない。共通状態byteを追加せず、実際に必要なfieldの値域またはschema固有presence bitmapで区別する。
V4対応readerは次の順序で処理する。
version == 4と判定した後だけbase header 46byteを読む。V1〜V3を46byte headerとして読んではならない。headerLength > 46ならdebug format ID/versionを読む。理解できないschemaはheaderLengthまでskipする。headerLengthから、recordLength刻みでレコードを読む。0以上のframeだけを通常軌跡へ復元する。負値frameと未知suffixはskipする。(fileSize - headerLength) % recordLengthの余りは、電源断等による末尾の不完全レコードとして切り捨てる。負値frameを event(EVENT_START/EVENT_CONTINUATION、SESSION_FINALIZED検出を含む)として解釈するのは
debugFormatId/version == 1/1(NAVIGATION_FIELD_TEST_DEBUG v1)のときだけとする。未知schema・未知version・
debug section無しでは、負値frameは意味を解釈せず単純にskipし、正常終了判定(cleanlyFinalized)もしない。
デバッグ機能を持たないreaderも、V4 base layoutを理解していれば、デバッグなし・ありの両方から同じ通常軌跡を復元できる。
46/13で作成する。Issue #131以降、通常の新規記録はTRAJECTORY_POSITION_QUALITY(ID 2、50/27)で作成し、46/13はschemaを持たないreader/writer向けの最小fallback形式として残る。.dtrkへrenameする。rename前の一時fileは一覧・再生・自動回復の対象外とする。segmentIdのまま、地図上で線がつながる)。exported/への移動)の対象にしない。この保護はruntime状態であり永続化しない。UIの選択制御だけに依存せず、削除・エクスポートAPI側でも同じ条件で除外する(#125)。SESSION_FINALIZED(USER_STOPPED)を書いてwriterを閉じ、live writerの所有と再開対象を解除する。新しい記録は開始しない。確定終了後のfileは通常どおり選択・削除・エクスポートでき、次の「記録を再開」は同一fileへ戻らず新しいsegmentIdと新しいfileを開始する。最初のfixより前(temp fileも未作成)に確定終了した場合は、不要なtempを残さず、.pending-plan.jsonの非実走計画契約にも影響しない(#125)。segmentIdと新しいfileを開始する。旧fileはreaderの末尾切り捨てだけで回収し、writerは検査・修復しない。プロセス終了後は上記の削除・エクスポート保護を持ち越さない(live writerを失った旧fileは通常の確定済みfileとして扱う)。recordLengthを新fileへ引き継ぎ、base headerのsegment IDと基準点は新しい最初のfixから作る。droppedEventCountへ加算する。fixを一度も得ずに終了した計画は後述の非実走計画JSONへ保存する。.pending-plan.jsonへtemp→flush→renameで保存する。最初のdebug DTRKをrenameできた後にpending fileを削除する。再起動時に旧debug DTRKが無くてもpending fileからheaderを復元できる。計画を実走せず終了した場合はpending fileを正式な.plan.jsonへrenameする。.pending-plan.jsonと、それに対応するdebug DTRK(同一sessionId)が両方存在する場合は、DTRKを正としpending fileを破棄する(DTRK rename成功後・pending削除前の電源断による残留)。.pending-plan.jsonを読むときは、一括確保する前にファイルサイズを1 MiB上限と照合する。上限超過・I/O失敗・読み取り中の消失・切り詰め・不正JSON・再encode不能はすべて「復元不能」として扱い、例外をRtkForegroundService起動まで伝播させない。同じ検査は正式.plan.jsonへの昇格経路と記録一覧の.plan.json読み込みにも適用し、1件の壊れた・巨大なファイルで起動や一覧を落とさない。46/13の新fileを開始し、DEBUG_CONTEXT_LOSTを既存診断ログへ記録する。V4のまま行える変更:
debugFormatIdの追加。debugFormatVersion追加。46/13)と、任意の既知・未知debug schemaを持つファイルの混在。本体versionを上げる必要がある変更:
headerLength/recordLengthの解釈を変える変更。| debugFormatId | debugFormatVersion | 名前 | debugHeaderPayload | debugRecordSuffix |
|---|---|---|---|---|
1 |
1 |
NAVIGATION_FIELD_TEST_DEBUG |
無圧縮UTF-8 JSON、1MiB以下 | 14byte、recordLength=27固定 |
2 |
1 |
TRAJECTORY_POSITION_QUALITY |
0byte(headerLength=50固定) |
14byte、recordLength=27固定 |
debugFormatId番号は一度公開した意味へ再割当しない。record suffixのoffset・型・意味を互換に保てない変更では、同じIDのdebugFormatVersionを上げる。DTRK本体versionは変更しない。
「debug」という語について: offset 46-49のフィールドは実装・現行文書でdebugFormatId/debugFormatVersionと呼ぶが、wire上の役割は「V4 base layoutを拡張するschema識別子」である。ID 2(TRAJECTORY_POSITION_QUALITY)は現地試験専用ではなく通常の走行記録が使うため、「debug」は歴史的な名称にすぎない。既存シンボルの一括改名は行わず、この対応で読む。
| wire上の役割 | 実装のコード名 |
|---|---|
| extension schema ID / version | debugFormatId / debugFormatVersion |
| extension header payload | debugHeaderPayload |
| record suffix | debugRecordSuffix |
現地試験で使ったplanner入力・planner結果と、各測位点の品質を同じ.dtrkだけから再構成するdebug schema。debugFormatId=1、debugFormatVersion=1とする。
debugHeaderPayloadはBOMなし・無圧縮UTF-8 JSONのobjectとする。JSON numberにNaN・Infinityを使用しない。浮動小数点は表示丸めせず往復可能表現、整数は64bit符号付きで読む。objectのkey順は意味を持たず、readerは未知keyを無視する。次のtop-level fieldを必須とする。
| field | 型 | 内容 |
|---|---|---|
sessionId |
string | 通常は試験session UUID。相関不能時は予約ID UNLINKED |
capturedAtMs |
integer | このplan snapshotを確定したepoch ms |
app |
object | アプリ・build・端末情報 |
planner |
object | plan type/patternと適用契約 |
request |
object | plannerへ渡した4要素の完全snapshot |
result |
object | TillagePlanPipelineResultの完全snapshot |
app| field | 型 | 内容 |
|---|---|---|
versionName |
string | 製品versionとbuild IDを含むmanifest値 |
versionCode |
integer | APKのversionCode |
buildId |
string | Issue #120で定義したbuild識別子 |
gitSha |
string/null | buildへ埋め込まれたsource revision。取得不能時のみnull |
deviceModel |
string | 記録端末model |
planner| field | 型 | 内容 |
|---|---|---|
patternId |
string | TillagePlan.patternId(コード定数TEXTBOOK_BASIC_TILLAGE_PATTERN_IDが出所)。現行Pattern 1はtextbook-basic-tillage |
patternVersion |
integer | TillagePlan.patternVersion |
contractId |
string | 適用した実装契約runbookのbasename。現行はnavigation-field-geometry-pipeline-contract-pattern-1。track-coreのコード定数を出所とし、自由文字列を仕様へ直書きしない |
契約内容のsource revisionはapp.gitShaで特定する。契約に別のversion番号は設けずgitShaへ一本化する。planner契約とDTRK本体version、debug format versionを混同しない。
requestTillagePlanRequestの4要素だけを保存し、保存のための追加planner入力を作らない。
| field | 型 | 内容 |
|---|---|---|
field |
object | polygonUuidと全polygons。各polygonはshellとholes、各座標は[lat, lon] |
entrance |
object | entranceId、boundaryPoint:[lat,lon]、approachHeadingDeg、source、circulationAtCorner |
profile |
object | profileId、profileName、implementWidthCm、overlapWidthCm、offsetForwardCm、offsetLateralCm、workPitchCm |
centerDirection |
object | kind、headingDeg、candidateId。candidateIdはEDGE_ALIGNED以外null |
entrance.source、entrance.circulationAtCorner、centerDirection.kindはKotlin enum/sealed subtypeの識別名を大文字snake caseで保存する。entranceIdは登録入口だけstring、予測・実横断復元入口はnull。circulationAtCornerは角入口だけ値を持ち、それ以外はnull。candidateIdはEDGE_ALIGNEDだけ値を持ち、それ以外はnullとする。
入口・中心耕・actionのheadingは真北0度・時計回り・[0, 360)であり、field-geometry契約の表現をそのまま保存する。入口座標とheadingは表示用に丸めず、plannerへ渡したDouble値をJSON numberで保存する。
resultproduction serviceが返したTillagePlanPipelineResultを欠落なく保存する。top-levelのstatus、stoppedStage、summaryに加え、tillageRegionsResult、fixedTillageConstraintResolutionResult、finalTillagePlanResultを含む。上位層で表示用に再構成した要約だけを保存して、各Stageの生の結果を捨ててはならない。
debug .dtrk headerではstatus=SOLVEDかつfinalTillagePlanResult=Solvedを必須とする。Stoppedを含む結果は後述の失敗・非実走計画JSONだけで使用する。
finalTillagePlanResultがSolvedの場合: FinalTillagePlanを欠落なく保存する。少なくともplan.actionsのorder、phase、roleとrole固有値、actionType、circulation、全geometry:[lat,lon]、overlapAdjustment、warnings、deadheadCoverage、tilledCoverage、perimeterResidual、centralUncovered、centerTillageWorkAreasを含む。finalTillagePlanResultがStoppedの場合: stage、reason、detail、warnings、diagnosticGeometry、型付きstopCauseを欠落なく保存する。部分計画をSolvedとして保存しない。enum、sealed subtype、warning、stop causeは表示文言ではなく安定した型識別名と、その型が持つ全fieldをobjectとして保存する。readerが未知の型識別名を見つけた場合も、JSON object自体は保持・表示できなければならない。
schema 1のpayloadは1〜1,048,576byteとする。readerは一括確保前に長さを検証する。writerはpolygon、action、diagnosticを黙って間引いたり、表示座標へ丸めたりして上限へ合わせてはならず、超過時はdebug記録開始を失敗させる。payloadが不正UTF-8、parse不能、必須field欠落でも通常軌跡は復元し、plan再構成だけを不可とする。result全量が一次情報であり、同じrequestとgitShaでplannerを再実行した結果のbit単位一致は保証しない。
V4ヘッダは記録開始後に書き換えない。NAVIGATION_FIELD_TEST_DEBUGを持つ.dtrkは、planner結果がSOLVEDとして利用者に確定された後、最初の測位frameより前に新規作成する。既存fileへpayloadを後付けせず、進行中fileを終了して新fileへ切り替える。並行する2 writerは作らない。
各通常測位frameの元になったGGA fixと、その時点で紐付いたRMC/GSTの品質情報を保存する。schema 1はrecordLength=27固定であり、通常測位prefixの後ろへ次の14byteを置く。
| offset(slot内) | サイズ | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 1byte | UInt8 | ggaFixQuality |
生GGA fix quality。0=このslot全体がNOT_RECORDED |
| 1 | 1byte | UInt8 | numSatellites |
使用衛星数。0xFF=個別値なし |
| 2 | 2byte | UInt16 | hdopCenti |
HDOP×100を四捨五入。0xFFFF=個別値なし |
| 4 | 2byte | UInt16 | gstHorizontalErrorMm |
GST水平誤差m×1000を四捨五入。0xFFFF=個別値なし |
| 6 | 4byte | Int32 | altitudeCm |
GGA標高m×100を四捨五入。0x80000000=個別値なし |
| 10 | 2byte | UInt16 | rmcAgeMs |
base record時刻から直近RMCまでの経過ms。0xFFFF=個別値なし |
| 12 | 2byte | UInt16 | gstAgeMs |
base record時刻から直近GSTまでの経過ms。0xFFFF=個別値なし |
ggaFixQuality=0なら残り13byteにかかわらずslot全体をNOT_RECORDEDとする。numSatellitesは0〜254、UInt16値は0〜65534、altitudeCmは-2147483647〜2147483647を有効範囲とし、型の残る値を「値なし」に予約する。ageはGGA処理時壁時計と直近RMC/GST処理時壁時計との差をmax(0, difference)で求める。上限超過はセンチネルと衝突しない最大値へclampする。
schema 1は負値frameをevent frameとして使い、GNSS点がない間も同じ27byte固定長列へeventを追記する。先頭Int32が-1ならEVENT_START、-2ならEVENT_CONTINUATION、その他の負値は将来予約。1eventの全frameは他frameを挟まず連続して書き、完了後にflushする。
EVENT_START (elapsedMs=-1):
| offset | size | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4 | Int32 | marker | -1 |
| 4 | 8 | Int64 | eventAtMs |
event発生時のepoch ms。最初のfixより前も絶対時刻で保持 |
| 12 | 4 | UInt32 | eventSequence |
file内で1から単調増加 |
| 16 | 2 | UInt16 | eventType |
下表 |
| 18 | 2 | UInt16 | payloadLength |
UTF-8 JSON byte数。最大65535 |
| 20 | 7 | byte列 | payload先頭 | 不足分は0 padding |
EVENT_CONTINUATION (elapsedMs=-2):
| offset | size | 型 | 名前 | 内容 |
|---|---|---|---|---|
| 0 | 4 | Int32 | marker | -2 |
| 4 | 4 | UInt32 | eventSequence |
STARTと同じ値 |
| 8 | 2 | UInt16 | chunkIndex |
1から連番 |
| 10 | 17 | byte列 | payload続き | 最終chunk不足分は0 padding |
readerはpayloadLength分だけを連結する。sequence/chunkIndex不一致、途中欠落、不正JSONのevent全体を無視し、通常軌跡は維持する。writerは65535byte超過時に自由文detailだけをUTF-8境界で短縮してtruncated=trueを付け、構造化fieldは落とさない。
| eventType | 名前 | payload必須field |
|---|---|---|
| 1 | RECORDING_STARTED |
eventName、reason: USER / PROCESS_RESTART / FORMAT_CHANGED / PLAN_CONFIRMED / CLEAR_CONTINUATION |
| 2 | RECORDING_PAUSED |
eventName、reason: USER / SYSTEM |
| 3 | RECORDING_RESUMED |
eventName |
| 4 | PLAN_CONFIRMED |
eventName、planRevision(1以上) |
| 5 | NAVIGATION_STATE_CHANGED |
eventName、from(nullable string)、to、reasonCode(nullable string) |
| 6 | TARGET_ACTION_CHANGED |
eventName、fromActionIndex/toActionIndex(nullable integer)、trigger |
| 7 | USER_OPERATION |
eventName、operationId(安定ID)、value(無ければnull) |
| 8 | WARNING |
eventName、code、stage/detail(nullable string) |
| 9 | ERROR |
eventName、code、stage/exceptionClass/message(nullable string) |
| 10 | SESSION_FINALIZED |
eventName、outcome、endedAtMs、pointRecordCount、eventCount、droppedEventCount、writeErrorCount、lastActionIndex、coverage |
eventNameは表の名前と一致させる。state、reason、operation、codeは表示文言ではなく安定IDを保存する。未知eventTypeはskipする。
NAVIGATION_STATE_CHANGED(5)、TARGET_ACTION_CHANGED(6)、SESSION_FINALIZEDのcoverageは走行ナビゲーション実行機能(後続Issue)を前提とした前方定義であり、その実装Issueでpayload fieldが改訂されうる。未実装の間はこれらのeventを生成せず、coverageはnullとする。
USER_OPERATION.operationIdは少なくともTRAJECTORY_START、TRAJECTORY_PAUSE、TRAJECTORY_RESUME、TRAJECTORY_NEW_SEGMENT、TRAJECTORY_CLEAR、PLAN_USE、PLAN_REPLAN、NAVIGATION_START、NAVIGATION_STOP、ROTARY_RAISE、ROTARY_LOWER、MANUAL_INTERVENTION_START、MANUAL_INTERVENTION_ENDを予約する。該当機能が未実装のIDはeventを生成しない。
SESSION_FINALIZED.outcomeはCOMPLETED、USER_STOPPED、FORMAT_CHANGED、PLAN_REPLACED、CLEAR、PROCESS_EXIT、WRITE_FAILEDのいずれかとする。coverageは取得可能な場合にplannedTilledAreaM2、executedTilledAreaM2、coveredRatio、remainingAreaM2、completedActionCount、totalActionCountを持ち、検証器が未実装または値を確定できない場合はobject全体をnullとする。
SESSION_FINALIZEDは正常にfileを閉じる際の最後のeventであり、headerを更新しない。final eventが無ければ中断終了、final eventより後にframeがあれば不正fileとする。
各測位点の測位品質を、現地試験用の計画JSON・event frameを持たずに保存するための拡張schema(Issue #131、2026-09-07仕様策定)。通常の新規DTRK記録はこのschemaで記録する。低品質点も解析・監査に必要な一次データとして捨てず全点保存し、色分け・除外は表示時の非破壊フィルタで行う(表示仕様はP6 UI仕様「走行軌跡の測位品質記録・色分け・除外」・PC側 耕耘あと表示検証ツールを参照)。
debugFormatId=2、debugFormatVersion=1。debugHeaderPayloadは0byte。したがってheaderLengthは50固定(base 46 + debugFormatId 2 + debugFormatVersion 2)。recordLength=27固定。通常測位prefix 13byteの後ろへ、schema 1と同一レイアウトの14byte position suffix(前掲「14byte position debug suffix」)を置く。(ID 1, version 1)と(ID 2, version 1)の組だけを同じcodecで読み、未知versionのsuffixは品質として解釈せずskipする(既知部分=通常測位prefixだけ回収)。headerLength != 50のとき不正とする。recordEvent/pending event/SESSION_FINALIZEDはID 2では常にno-op。よってID 2ファイルにcleanlyFinalizedの概念はない(通常V4 46/13と同じく、中断/正常終了を区別しない)。headerLength==50のときdebugHeaderPayloadを空ByteArrayではなく**nullへ正規化**して返す。debugHeaderPayload != nullを計画JSON存在の判定に使う既存consumerは、先にdebugFormatId==1でgateし、ID 2・未知IDのpayloadをJSONとしてdecodeしない。supportsEventsはsuffix長ではなくdebugFormatId==1で判定する(ID 2も14byte suffixを持つため、suffix長で判定すると通常記録が意図せずevent対応になる)。records/へ保存する(records/debug/はID 1専用)。writer factoryは plain 46/13 / 通常品質 ID 2 / NAV debug ID 1 の3ブランチを明示し、debugHeaderPayloadの有無を保存先・factory選択の代理条件にしない。debugFormatIdとしてheaderLengthまでskipし、recordLength=27刻みで通常測位prefix 13byteだけを回収する(「readerの必須動作」2〜4)。位置・時刻・COG・速度は読めるが品質は見えない。46/13 V4は品質を持たず、全点UNKNOWN(品質記録なし)として扱う。品質をFIX等と推測しない。NOT_COMPLETED、INVALID_INPUT、またはSOLVEDでもfixを一度も記録せず終了した計画は、.dtrkを捏造せずrecords/debug/へ単独JSONとして保存する。内容はheader payloadと同じschemaで、finalTillagePlanResult=Stoppedと取得済みpartialを許す。
ここでいう「実走した」は、記録形式に依存しない: 計画確定後に軌跡記録へfixを1点以上書いたこと(通常46/13でもdebug 27でもよい)を指す。debug DTRKを作ったかどうかとは別概念で、debugモードOFFのまま確定して通常V4へ走行した計画も「実走した」に含める。よって、実走した計画に対しては非実走JSONを生成せず、plan ended without drivingの診断や不要なFORMAT_CHANGED境界も出さず、進行中の記録・計画コンテキストを壊さない。「1点も記録していないSOLVED計画」だけが非実走.plan.jsonの対象になる。
ファイル名は{yyyyMMdd-HHmmss-SSS}_{sessionId}.plan.json、衝突時は-01から連番とする。相関可能なら同じ試験のdebug .dtrkとUUIDを共有する。相関先が無い、または通常46/13 DTRKしかない場合はsessionId="UNLINKED"とし、関連付け可能であるかのようなIDを生成しない。単独JSONもBOMなし・無圧縮UTF-8、1MiB上限、一時fileへのflush後renameとする。.pending-plan.jsonは一覧・書き出し対象外だが、起動時回復の対象とする。
P6ナビゲーション(Issue #60)Stage 0(Track #61)で、Profileへ重ね幅(overlapWidthCm)を追加した際には次の判断をしていた。
offsetForwardCm/offsetLateralCm(V2)・implementWidthCm(V3)は、記録済みのアンテナ位置から作業機中心の耕耘あと帯を描画・再生する際に必要な値であり、「その記録がどの設定で作られたか」を軌跡ファイル自身に残す監査目的でヘッダへ持たせている。アンテナ中心線にはこれらのオフセットを適用しない。Profile(SharedPreferences)にのみ保存し、.dtrkへは追加しない。ナビゲーション計画の保存形式は、これを導入するStageで別途仕様化する。DtrkFormat/DtrkFileWriter/DtrkFileReaderはStage 0では変更していない。Issue #96 / Track #121では、現地ナビ検証後にテスト・デバッグへ必要な計画条件、実走、測位品質、ナビ状態を同じ.dtrkから再現することを優先し、利用者承認の上で「ナビゲーション計画を.dtrkへ入れない」というStage 0時点の判断を改訂する。これはplanner入力・Stage・候補選択を変える契約変更ではなく、現行plannerの入力snapshotと出力・実行状態を記録する保存契約の変更である。