3_Androidタブレット対応_実装案.md の「9. フェーズ別実装計画 / フェーズ2」を、track #14(親Issue #3、フェーズ1: docs/decisions/p3-phase1-spike.mdのadopted判断を受けた続き)の実行可能な手順に分解したもの。
フェーズ1と異なり、対象実機・DG-PRO1RWSは不要。開発環境(JDK 21 / Android SDK / Gradle 9.1.0、~/android-sdk)はフェーズ1で構築済みのため、そのままCLI(WSL)で完結できる。
Issue #14のCODEXレビュー(GGA/RMC直接テストの見落とし、JSON解析手段の未定義、テスト件数の誤り、commit境界の4点)を反映済み。反映箇所は各節に記載。
| 項目 | 内容 |
|---|---|
| フェーズ1の状態 | track #13 完了(outcome=adopted)。apps/android-tablet/のビルド・lint・testが成功する状態から開始する |
| 移植元(Python) | tools/map/line_assembler.py、tools/parse/gnss_stream_stats.py(nmea_checksum/decode_gga/decode_rmc/nmea_coord_to_decimal)、tools/map/fix_state.py、tools/map/replay_source.py |
| 移植元テスト | tools/map/tests/test_line_assembler.pyはLineAssemblerTests(6件、バッファリング関連)とParseSentenceTests(3件、checksum/フィールド分割関連)に分かれる。後者はNmeaParser.ktへ移植するため、line assembler側は6件として数える(CODEXレビュー反映、9件のまま数えるとparser側の3件と二重計上になる)。加えてtools/parse/tests/test_gnss_stream_stats.pyのうちdecode_gga/decode_rmc直接test(2件)、tools/map/tests/test_fix_state.py(15件、内訳は3節参照)、tools/map/tests/test_replay_source.py(8件) |
| 移植先(Kotlin) | 実装案4節のディレクトリ案どおり nmea/NmeaLineAssembler.kt、nmea/NmeaParser.kt、model/FixState.kt、model/ConnectionState.kt、data/FixRepository.kt、data/ReplayNmeaSource.kt |
| 統合確認用fixture | samples/map-fixtures/minimal.nmea + .meta.json(8センテンス、checksum_ok=7/fail=1、GNGGA=5/GNRMC=3) |
| テスト方式 | JUnit4(AGPの標準JVM unit test構成をそのまま使う)。JUnit5導入(de.mannodermaus plugin等)はフェーズ1で経験した「新DSLの検証コスト」と同種のリスクを増やすため見送る。@Testを複数ケース分並べる方式とし、JUnit4の@RunWith(Parameterized::class)は使わない(ケースごとにアサーション内容が異なり、機械的な入出力比較ではないため)。「parameterized testの要件を満たす」という表現はしない |
| JSON解析(CODEXレビュー反映) | .meta.jsonの解析はAndroid標準のorg.json.JSONObjectを使う。ただしJVM unit testではorg.jsonがstub化されているため、testImplementation("org.json:json:20260522")を追加して実体を使わせる。実装コード自体は追加ライブラリ無しで動く(端末上はorg.jsonが実体を持つため)。(実装結果反映) org.jsonは小数表記の数値(例: "duration_actual_s": 1.0)をDoubleではなくjava.math.BigDecimalとして返す。JSONObject.opt()の戻り値をInt/Long/Doubleのみで型判定すると小数値がすべて弾かれ、意図せずデフォルト間隔にフォールバックするバグになる(実装時にhigher speed shortens sleep intervaltestで検出)。Number型で広く受けてtoDouble()する実装にすること |
| 非同期処理(CODEXレビュー反映) | MutableStateFlowを使うため、kotlinx-coroutines-coreをCompose経由の推移依存に任せずimplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")として明示的に追加する |
(CODEXレビュー反映) 依存関係の追加だけを独立コミットにすると「単独で意味のある実行単位」にならないため、最初の実装であるこのステップに同梱する。
app/build.gradle.kts の dependencies に以下を追加する
testImplementation("junit:junit:4.13.2")testImplementation("org.json:json:20260522")implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")tools/map/line_assembler.py の LineAssembler / LineTooLongError を nmea/NmeaLineAssembler.kt に移植する。バイト列(ByteArray)のバッファリング、\r\nと単独\nの両対応、max_line_length超過時の例外化とバッファ破棄はPython実装の挙動をそのまま踏襲するsrc/test/java/.../nmea/NmeaLineAssemblerTest.kt に、test_line_assembler.pyのLineAssemblerTestsクラス相当のtestを移植する| 移植元test | 確認内容 |
|---|---|
test_single_chunk_with_multiple_sentences |
1チャンクに複数センテンスが含まれる場合、両方切り出せる |
test_sentence_split_across_multiple_chunks |
センテンスがチャンク境界で分割されても次のfeedで復元できる |
test_lone_lf_terminator_is_accepted |
\r無しの\n単独終端も受理し、\rは残さない |
test_non_ascii_bytes_do_not_crash_assembly |
非ASCIIバイトを含む壊れた行があっても後続の正常行の切り出しは継続する |
test_max_line_length_exceeded_raises_and_clears_buffer |
上限超過で例外を投げ、バッファを破棄し、以降の入力は正常処理できる |
test_incomplete_trailing_data_is_buffered_not_returned |
終端が来ていない末尾データは返さずバッファに残す |
./gradlew test でこれらが緑になること、./gradlew assembleDebug が引き続き成功することを確認する。
tools/parse/gnss_stream_stats.py の nmea_checksum / decode_gga / decode_rmc / nmea_coord_to_decimal と、line_assembler.py の parse_sentence($始まり判定、*以降のchecksum hex解析、フィールド分割)を nmea/NmeaParser.kt に移植する。Python実装と同じく、checksum検証とフィールド分割は「行を受け取ってセンテンス種別・フィールド・checksum可否を返す」関数として実装し、GGA/RMCの座標・品質デコードは別関数に分離する(Python側もこの2段構成のため、構造をそのまま踏襲できる)。
src/test/java/.../nmea/NmeaParserTest.kt に以下を移植する。
| 移植元test | 確認内容 |
|---|---|
test_parses_valid_sentence(line_assembler由来) |
正しいchecksumのセンテンスを種別・フィールド・checksumOk=trueで返す |
test_detects_bad_checksum |
checksum不一致をchecksumOk=falseで返す |
test_non_nmea_line_returns_none |
$で始まらない行はnullを返す |
test_decodes_gga_position_and_fix_fields(CODEXレビュー反映、tools/parse/tests/test_gnss_stream_stats.py由来) |
GGAの緯度経度・fix_quality・num_satellitesを正しくデコードする(期待値: lat≈35.702057, lon≈139.761315, fix_quality="1", num_satellites="08") |
test_decodes_rmc_status_and_position(CODEXレビュー反映、同上) |
RMCのstatus・緯度経度・日付を正しくデコードする(期待値: status="A", lat≈35.702057, lon≈139.761315, date="230394") |
(CODEXレビュー反映) 当初「GGA/RMCデコードの独立testは無い」としていたが誤りで、tools/parse/tests/test_gnss_stream_stats.pyに上記2件が存在するため移植対象に加えた。緯度経度変換は中核ロジックであり、FixRepository経由だけでなくparser単体で失敗箇所を切り分けられるようにする。
南半球/西半球(S/W)の符号反転はP2側にも直接testが無いため必須ではないが、移植時の境界確認としてNmeaParserTest.ktに1件追加することを推奨する(例: nmea_coord_to_decimal("3542.1234", "S")が負値になることの確認)。
tools/map/fix_state.py の FixState(dataclass)を model/FixState.kt(data class)へ、CONNECTION_STATES集合を model/ConnectionState.kt(enum: STARTING/CONNECTED/DISCONNECTED/RECONNECTING/EOF/ERROR)へ、FixStateStoreを data/FixRepository.kt へ移植する。
Python版はスレッド間共有のためthreading.Lockを使っているが、Kotlin版はMutableStateFlow<FixState>のupdate {}(atomic)で置き換え、明示的なlockは持たない。実装案6節の「StateFlow<FixState>は不変オブジェクトとして更新し、Bluetooth reader threadとUI threadの共有可変状態を作らない」という設計方針どおりにする。
src/test/java/.../data/FixRepositoryTest.kt に、test_fix_state.pyの4テストクラス15件(CODEXレビュー反映、当初14件としていたのはFixStateStoreConnectionStateTestsを3件と誤カウントしていたため)を移植する。
| 移植元testクラス | 確認内容 |
|---|---|
FixStateStoreValidUpdateTests(2件) |
有効なGGA/RMCで座標・fixValid・fixQuality・numSatellites・rmcStatus・sentenceCountが更新される |
FixStateStoreInvalidFixTests(4件) |
GGA fix_quality=0、RMC status=V、GGA緯度経度欠損は座標を上書きせずinvalidFixCountとlastInvalidReasonを更新する。無効の後に有効を受けるとfixValidが回復する |
FixStateStoreChecksumAndErrorTests(5件) |
checksum不正は座標・sentenceCountを更新せずchecksumFailCountのみ増やす。markErrorでerrorCount増加・lastError更新。有効センテンス受信後はlastErrorがクリアされる(errorCountは保持)。checksum不正ではlastErrorはクリアされない |
FixStateStoreConnectionStateTests(4件、CODEXレビュー反映) |
connected設定でconnectedAtが記録される(1件)、connected設定で過去のlastErrorがクリアされる(1件、別test)、eof設定でeofフラグが立つ(1件)、未知の状態は例外(1件) |
期待値の緯度経度(例: 33.204813 / 133.106672)はPython版と同じ入力データ(GNGGA,000001.00,3312.2888,N,13306.4003,E,...等)を使い、そのまま照合する。
tools/map/replay_source.py のreplay関数を、実装案の「ログ再生: Android instrumentation testと手動確認用に残す」方針に沿って data/ReplayNmeaSource.kt へ移植する。.meta.jsonからduration_actual_s / nmea.sentence_countで平均間隔を計算し、無い場合や不正な場合はデフォルト間隔(0.1秒相当)にフォールバックする挙動をそのまま踏襲する。JSON解析は0節で決めたorg.json.JSONObjectを使う。
src/test/java/.../data/ReplayNmeaSourceTest.kt に、test_replay_source.pyの8件を移植する。
| 移植元test | 確認内容 |
|---|---|
test_uses_metadata_duration_and_sentence_count |
metaがあればduration/sentence_countで間隔を計算する |
test_falls_back_to_default_when_metadata_missing |
metaファイル自体が無ければデフォルト間隔 |
test_falls_back_to_default_when_duration_is_zero_or_negative |
durationが0以下ならデフォルト間隔 |
test_falls_back_to_default_when_sentence_count_missing |
sentence_countが無ければデフォルト間隔 |
test_falls_back_to_default_when_metadata_is_not_valid_json |
JSON不正ならデフォルト間隔(org.json.JSONExceptionを捕捉する) |
test_replay_ingests_all_sentences_and_marks_eof |
全センテンスをFixRepositoryへ反映し、最後にeof状態にする |
test_higher_speed_shortens_sleep_interval |
speed倍率でsleep間隔が短くなる |
test_rejects_non_positive_speed |
speedが0以下なら例外 |
samples/map-fixtures/minimal.nmea(8センテンス、GNGGA=5/GNRMC=3、checksum_ok=7/fail=1)をReplayNmeaSourceに読ませ、以下がP2の記録値と一致することを確認する統合testを1件追加する。
sentenceCount == 7(checksum不正1件を除く)checksumFailCount == 1$GNRMC,000006.00,A,3312.2895,N,13306.4010,E,...由来(fixValid=true、rmcStatus="A")invalidFixCount == 3(gga_fix_quality_0が1件 $GNGGA,000003.00,...,0,00,...、rmc_status_vが1件 $GNRMC,000004.00,V,...、gga_missing_lat_lonが1件 $GNGGA,000005.00,,,,,... の内訳。checksum不正の1件はchecksum判定で弾かれinvalidFixCountには含まれない)./gradlew test
./gradlew lint
./gradlew assembleDebug
フェーズ1で成功していたビルド・lintを維持しつつ、testでステップ1〜4のtestがすべて緑であることを確認する。件数は6(line assembler) + 5(parser、GGA/RMC直接test 2件含む) + 15(fix_state) + 8(replay_source) + 1(minimal.nmea統合test) = 35件(CODEXレビュー再指摘反映: test_line_assembler.pyの9件を line assembler側にそのまま数え、parser側の3件〈test_parses_valid_sentence等〉と二重計上していたため38件は誤り。S/W符号反転testを追加した場合は36件)。
work_recordでコード変更を記録する(フェーズ1と同様、track #14に紐付ける)track_recordで残すtrack_finish(outcome="adopted")し、親Issue #3の本線を更新するdocs/decisions/への反映を検討する(フェーズ1のp3-phase1-spike.mdほどの分量は想定していない。差分が小さければtrack記録のみで十分)@RunWith(Parameterized::class)は、ケースごとにアサーション内容が異なるため今回は使わない方針としたorg.json:jsonのバージョン(20260522)とkotlinx-coroutines-coreのバージョン(1.11.0)は本手順作成時点のMaven Central最新版。着手時に変わっていないか確認する