diff --git a/android/app/build.gradle b/android/app/build.gradle index fc200206d..896b7c0e5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -161,12 +161,12 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.dev" versionNameSuffix "-dev" - versionCode 100000761 + versionCode 100000763 versionName "10.15.1" } prod { dimension "environment" - versionCode 100000761 + versionCode 100000763 versionName "10.15.1" } // ストア配信する dev (Canary) と applicationId を分けた、ローカル検証専用のフレーバー。 @@ -177,7 +177,7 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.local" versionNameSuffix "-local" - versionCode 100000761 + versionCode 100000763 versionName "10.15.1" } } diff --git a/android/wearable/build.gradle.kts b/android/wearable/build.gradle.kts index f5f67fb2c..2f8c3d6aa 100644 --- a/android/wearable/build.gradle.kts +++ b/android/wearable/build.gradle.kts @@ -85,12 +85,12 @@ android { dimension = "environment" applicationIdSuffix = ".dev" versionNameSuffix = "-dev" - versionCode = 100000762 + versionCode = 100000764 versionName = "10.15.1" } create("prod") { dimension = "environment" - versionCode = 100000762 + versionCode = 100000764 versionName = "10.15.1" } // :app の local フレーバーと対になるローカル検証専用フレーバー。Wear の Data Layer は @@ -100,7 +100,7 @@ android { dimension = "environment" applicationIdSuffix = ".local" versionNameSuffix = "-local" - versionCode = 100000762 + versionCode = 100000764 versionName = "10.15.1" } } diff --git a/app.config.ts b/app.config.ts index 44c9df221..d7e6f77d6 100644 --- a/app.config.ts +++ b/app.config.ts @@ -57,7 +57,7 @@ export default { userInterfaceStyle: 'automatic', // Expo SDK 57 の各モジュール(expo / expo-modules-core ほか)は podspec で iOS 16.4 以上を要求する deploymentTarget: '16.4', - buildNumber: '2910', + buildNumber: '2911', scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD', bundleIdentifier: IS_DEV ? 'me.tinykitten.trainlcd.dev' @@ -71,7 +71,7 @@ export default { ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', permissions: [], - versionCode: 100000761, + versionCode: 100000763, }, owner: 'trainlcd', experiments: { diff --git a/ios/Modules/VitsTTS/VitsTTSModule.swift b/ios/Modules/VitsTTS/VitsTTSModule.swift index f1d7760fe..b20a704d3 100644 --- a/ios/Modules/VitsTTS/VitsTTSModule.swift +++ b/ios/Modules/VitsTTS/VitsTTSModule.swift @@ -360,7 +360,7 @@ final class VitsTTSModule: NSObject { try outputs.withUnsafeMutableBufferPointer { outValues in try check( api.pointee.Run!( - session, nil, inNames.baseAddress, inValues.baseAddress, inputs.count, + session, nil, inNames.baseAddress, inValues.baseAddress, inValues.count, outNames.baseAddress, outputNameArgs.count, outValues.baseAddress), "Run") } diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj index 7e21982d8..dff3abb1c 100644 --- a/ios/TrainLCD.xcodeproj/project.pbxproj +++ b/ios/TrainLCD.xcodeproj/project.pbxproj @@ -2550,7 +2550,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; @@ -2590,7 +2590,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TrainLCD; @@ -2649,7 +2649,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2756,7 +2756,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2837,7 +2837,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2877,7 +2877,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -3089,7 +3089,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3141,7 +3141,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3192,7 +3192,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3250,7 +3250,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3301,7 +3301,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3358,7 +3358,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3407,7 +3407,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3459,7 +3459,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3678,7 +3678,7 @@ CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3734,7 +3734,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3784,7 +3784,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3842,7 +3842,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2910; + CURRENT_PROJECT_VERSION = 2911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/src/hooks/useWrongDirectionDetector.test.tsx b/src/hooks/useWrongDirectionDetector.test.tsx index 431410506..b8a061176 100644 --- a/src/hooks/useWrongDirectionDetector.test.tsx +++ b/src/hooks/useWrongDirectionDetector.test.tsx @@ -245,6 +245,45 @@ describe('useWrongDirectionDetector (hysteresis)', () => { expect(result.current.isLoopLineWrongDirection).toBe(false); }); + it('静止したまま小さなGPSノイズで揺れ続けても逆方向と判定されない', () => { + // #6967: 増加だけを累積し、ノイズトレランス以内の減少を相殺しないと、 + // 実際には動いていなくても累積が単調に伸びて閾値に達する。 + // 0.00009 度 ≒ 8m の往復なので、位置は 8m の帯から一歩も出ていない。 + const NOISE_LON_DELTA = 0.00009; + + const store = createStore(); + seedStore(store); + + const { result } = renderDetector(store); + + // 旧ロジックでは 76 サンプル目(38 往復)で累積が 300m に達していた + for (let i = 0; i < 200; i += 1) { + act(() => + setLocationLon( + store, + i % 2 === 0 ? BASE_LON : BASE_LON - NOISE_LON_DELTA + ) + ); + } + + expect(result.current.isWrongDirection).toBe(false); + expect(result.current.isLoopLineWrongDirection).toBe(false); + }); + + it('ノイズ以下の後退を挟んでも、実際に遠ざかっていれば逆方向と判定される', () => { + const store = createStore(); + seedStore(store); + + const { result } = renderDetector(store); + + act(() => setLocationLon(store, BASE_LON)); + + // 100m 級の西進のあいだに、ノイズトレランス以内(約 8m)の東進を挟む + advanceWest(store, [-0.001, 0.00009, -0.001, -0.001, -0.001, -0.001]); + + expect(result.current.isWrongDirection).toBe(true); + }); + it('公開フックは atom を読むだけで、Effectフックを呼ばなくても初期値を返す', () => { const store = createStore(); const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( diff --git a/src/hooks/useWrongDirectionDetector.ts b/src/hooks/useWrongDirectionDetector.ts index 64df72cf4..22ce75fdf 100644 --- a/src/hooks/useWrongDirectionDetector.ts +++ b/src/hooks/useWrongDirectionDetector.ts @@ -14,7 +14,7 @@ import { useNextStation } from './useNextStation'; // 連続何回の距離増加で逆方向と判定するか const WRONG_DIRECTION_CONSECUTIVE_COUNT = 4; -// 逆方向判定に必要な最小累積距離(m) +// 逆方向判定に必要な、最接近点からの最小の正味距離(m) const WRONG_DIRECTION_MIN_DISTANCE = 300; // GPSノイズとみなして無視する減少幅(m)。これ以下の減少では検知カウンタを維持する const WRONG_DIRECTION_NOISE_TOLERANCE = 10; @@ -56,8 +56,10 @@ export const useWrongDirectionDetectorEffect = (): void => { const prevDistanceRef = useRef(null); // 連続で距離が増加した回数 const consecutiveIncreaseCountRef = useRef(0); - // 連続増加中の累積距離(m) - const cumulativeIncreaseRef = useRef(0); + // 連続増加中に次駅へ最も近づいたときの距離(m)。「遠ざかった距離」はここからの + // 正味の差で測る。正の増加だけを足し込む累積だと、ノイズとして無視した小さな + // 減少が相殺されず、静止していてもGPSの揺れだけで単調に伸びる(#6967) + const nearestDistanceRef = useRef(null); // 通知済みのnextStation ID(undefinedは未設定、null/numberは通知済み駅ID) const notifiedForStationIdRef = useRef(undefined); // 前回のnextStation ID(駅変更検知用) @@ -89,7 +91,7 @@ export const useWrongDirectionDetectorEffect = (): void => { const resetDetectionState = useCallback(() => { prevDistanceRef.current = null; consecutiveIncreaseCountRef.current = 0; - cumulativeIncreaseRef.current = 0; + nearestDistanceRef.current = null; notifiedForStationIdRef.current = undefined; writeState(false, false); }, [writeState]); @@ -149,31 +151,43 @@ export const useWrongDirectionDetectorEffect = (): void => { const prevDistance = prevDistanceRef.current; prevDistanceRef.current = currentDistance; - // 初回測定は比較対象がないのでスキップ + // 初回測定は比較対象がないので、正味の増加を測る起点だけ置いて終了する if (prevDistance == null) { + nearestDistanceRef.current = currentDistance; return; } const increase = currentDistance - prevDistance; if (increase > 0) { - // 遠ざかった: 連続カウンタと累積距離を伸ばす + // 遠ざかった: 連続カウンタを伸ばす consecutiveIncreaseCountRef.current += 1; - cumulativeIncreaseRef.current += increase; } else if (increase < -WRONG_DIRECTION_NOISE_TOLERANCE) { - // 明確に近づいた: 連続カウンタをリセット + // 明確に近づいた: 連続カウンタをリセットし、起点も現在地へ張り直す + // (= 正味の増加を 0 に戻す)。 // ただし wrongDirectionDetected 自体は据え置き、通知発火済みフラグも維持する。 // 一度逆方向と判定された後、GPSノイズで一時的に近づいたとしても、 // 次駅が変わるか到着するまでは「逆方向状態」のまま扱う consecutiveIncreaseCountRef.current = 0; - cumulativeIncreaseRef.current = 0; + nearestDistanceRef.current = currentDistance; } - // 上記いずれにも当てはまらない小さな減少は GPS ノイズとして無視する + // 小さな減少は GPS ノイズとして連続カウンタを落とさない(ヒステリシス)。 + // ただし起点の更新は増減に関わらず行うため、距離そのものには反映される。 + + // 起点より近づいたら起点を更新する。無視した小さな減少もここで効くので、 + // 静止中の揺れでは正味の増加が伸びない + const nearestDistance = + nearestDistanceRef.current == null + ? currentDistance + : Math.min(nearestDistanceRef.current, currentDistance); + nearestDistanceRef.current = nearestDistance; + // 最接近点からの正味の増加距離(m) + const netIncrease = currentDistance - nearestDistance; if ( consecutiveIncreaseCountRef.current >= WRONG_DIRECTION_CONSECUTIVE_COUNT && - cumulativeIncreaseRef.current >= WRONG_DIRECTION_MIN_DISTANCE + netIncrease >= WRONG_DIRECTION_MIN_DISTANCE ) { // 同一のnextStationに対して既に通知済みならスキップ if (