diff --git a/docs/DEVICE_VALIDATION_LEDGER.md b/docs/DEVICE_VALIDATION_LEDGER.md index 6df0a492..c8c30152 100644 --- a/docs/DEVICE_VALIDATION_LEDGER.md +++ b/docs/DEVICE_VALIDATION_LEDGER.md @@ -17,6 +17,7 @@ | # | Özellik | Nerede | Kabul ölçütü (cihazda ne gözlemlenmeli) | Eklendi | |---|---------|--------|------------------------------------------|---------| +| 34 | **🔴 Device Validation Pending — Vehicle HAL Foundation** — CarOS Pro'daki TÜM modüllerin araçla YALNIZ bu katman üzerinden konuştuğu tek resmi Hardware Abstraction Layer. Bugün araç verisi Native VHAL/CAN(Hiworld/K24/NWD)/OBD/GPS → SignalNormalizer → VAL → VehicleCompute.worker → VehicleState → `useUnifiedVehicleStore` (**~40 dosyada** okunuyor) + Assistant Context + Battery Service + gauge SAB ile AYRI okunuyor; per-signal quality/confidence/source/stale/supported/unit veren ORTAK API yoktu. `VehicleHal` bu boşluğu doldurur. **15 sinyal kataloğu** (speed/rpm/coolant_temp/oil_temp/fuel_level/battery_voltage/throttle/ambient_temp/odometer/gear/reverse/ignition/door_state/parking_brake/tpms), her biri `supported=false` tohumlu (kaynak yok). **Signal modeli:** id·value·quality(low/medium/high/unknown)·confidence(0-1)·source(native/can/obd/deep_scan/inferred/none)·timestamp·stale·unit·supported. **Snapshot:** revision·updatedAt·signals(deep-freeze, id-sıralı). **API:** getSnapshot/getSignal(id)/hasSignal/subscribe/unsubscribe/refresh/reset/dispose + tipli erişimciler (getSpeed/getRPM/getCoolantTemp/getFuelLevel/getBatteryVoltage/getIgnition/getGear/getDoorState/getReverse/getThrottle/getOdometer/getTpms) + getVehicleIdentity + getCapability(HAL-yerel). **HAL ilkesi:** donanımdan bağımsız, KAYNAĞI GİZLER, aynı API her araçta çalışır, veri yoksa null/supported=false (fail-closed), hiçbir tüketici CAN/ELM327/Hiworld bilmez. **Veri ÜRETMEZ/YORUMLAMAZ** — yalnız tip doğrulama (number sinyale boolean/NaN→null), magic sanity eşiği YOK. **Gizlilik:** HAM VIN reddedilir (getVehicleIdentity yalnız fingerprint hash 8-64 hex, 17-karakter=VIN reddedilir), protokol sanitize, ham CAN frame/OBD response SAKLANMAZ. **YALIN:** SystemBoot/native/VAL/UnifiedVehicleStore'a BAĞLANMAZ (import YOK — tamamen bağımsız foundation); timer YOK; polling YOK; import yan etkisiz; yeni bağımlılık yok; Map O(1). Bounded (katalog sabit 15 · listener 32 · tpms 6); immutable (snapshot/signal deep-freeze); fail-soft (provider/listener izole, throw yok); duplicate event önleme (değişmezse emit yok); stale (yaş>5000ms veya saat-geri). ⚠️ **Gerçek kaynak wiring (VAL/UnifiedVehicleStore → HAL.ingest) + modüllerin HAL'e taşınması AYRI PR'lar.** | `platform/vehicleHal/vehicleHal.ts` (yeni), `platform/vehicleHal/index.ts` (yeni), `__tests__/vehicleHal.test.ts` (yeni, 35 test) | Gerçek araçta (kaynak wiring PR'ı bağlandıktan SONRA): (1) `getSpeed()/getRPM()/getBatteryVoltage()` gerçek değer döner (CAN/OBD/HAL kaynağından, tüketici kaynağı BİLMEDEN); (2) kaynağı olmayan sinyal (ör. `getIgnition()` şu an) null/supported=false; (3) sinyal eskirse stale=true (taze gösterilmez); (4) bir provider düşse HAL çökmez, diğer sinyaller okunur; (5) HAM VIN hiçbir yere sızmaz (yalnız fingerprint hash); (6) BASIC_JS/Mali-400'de HAL CPU/RAM regresyonu yok (O(1) lookup, timer yok, pasifken sıfır yük); (7) tüm modüller CAN/OBD/Hiworld yerine yalnız HAL kullanır. ⚠️ **Gerçek kaynak wiring + modül taşıma SONRAKİ PR KAPSAMI.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 33 | **🔴 Device Validation Pending — Capability Registry Foundation** — CarOS Pro'nun cihaz/araç/yazılım/servis yeteneklerini TEK merkezden, KANITA DAYALI, confidence içeren, DeviceTier-uyumlu yöneten registry. İleride Deep Scan · Vehicle HAL · Navigation · Eagle Eye · Companion AI · Assistant Context · Health/Remote/Fleet/Vision/Media OS · OEM/White-label · Plugin Runtime ORTAK KAYNAK olarak kullanacak. `CapabilityRegistry` Map-tabanlı O(1); 13 domain · 6 status (available/unavailable/unknown/degraded/restricted/unsupported) · 12 source (native/can/obd/deep_scan/config/license/runtime/network/user/oem/inferred/none) · quality(low/medium/high/unknown) · confidence(0–1 clamp). **~63 ID'lik başlangıç kataloğu** (device/vehicle/navigation/ai/vision/remote/platform/oem/fleet) `unknown` olarak tohumlu (KANIT YOK). **Karar kuralları (zero-trust/fail-closed):** available YALNIZ yeterli kanıtla; unknown≠available; **config tek başına fiziksel donanımı (device/vehicle/vision) AÇMAZ** → unknown; UI varlığı kaynak DEĞİL; stale kanıt available tutmaz→unknown; **çelişkili authoritative kaynak→unknown**; authoritative(native/can/obd/deep_scan/license/oem) yardımcıdan üstün; **DeviceTier minimumu karşılanmazsa→restricted** (Mali-400/low'da advanced GPU + local model + ağır vision restricted); **safety-critical writable (vehicle.coding/adaptation/bidirectional) yalnız authoritative + confidence≥0.85**; tek zayıf inferred safety-critical açamaz; confidence<0.5/quality low→degraded. Confidence kaynak-tabanlı (native 0.98…config 0.3, politika sabiti). **Güçlü cihaz varlık UYDURMAZ** — tier yalnız minimum engelini kaldırır, kanıt yine şart. API: register/update/remove/get/has/isAvailable/getStatus/getConfidence/list/listByDomain/resolveCapability/createSnapshot/evaluateRequirements/explainUnavailable/subscribe/reset/dispose. **YALIN:** SystemBoot/provider/UI'ya BAĞLANMAZ; import yan etkisiz (yalnız `DeviceTier` TYPE import, donanım probu/timer/native YOK); kalıcı storage YOK (runtime-memory); yeni bağımlılık yok. Bounded (256 capability · 32 listener · details16/limitations8); immutable (record/snapshot deep-freeze, nested dahil); privacy (VIN/MAC/koordinat/ham/secret sanitize); fail-soft (bozuk kayıt/provider/listener izole, throw yok); duplicate merge deterministik; değişmezse event YOK. | `platform/capability/capabilityRegistry.ts` (yeni), `platform/capability/index.ts` (yeni), `__tests__/capabilityRegistry.test.ts` (yeni, 40 test) | Gerçek cihazda (hardware provider wiring PR'ı bağlandıktan SONRA): (1) GPS/kamera/Bluetooth/Wi-Fi capability durumları doğru (native kanıtla available); (2) donanımı olmayan özellik available GÖRÜNMEZ (unknown/unavailable); (3) TPMS kaynağı olmayan araçta `vehicle.tpms` aktifleşmez; (4) Deep Scan keşifleri araç capability'lerine (`vehicle.live_pid`/`did_discovery`…) doğru yansır; (5) BASIC_JS/Mali-400'de registry CPU/RAM regresyonu yok (O(1) lookup, timer yok, pasifken sıfır yük); (6) capability değişiklikleri duplicate event üretmez; (7) unknown capability güvenli şekilde kapalı davranır (isAvailable=false). ⚠️ **Provider wiring · persistence · OEM config · SystemBoot wiring AYRI PR KAPSAMI.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 32 | **🔴 Device Validation Pending — Deep Scan Orchestration Foundation** — Deep Scan'in tüm katmanlarını (Runtime state machine · Persistence · Ignition Source) yöneten TEK koordinatör. `DeepScanOrchestrator` deterministik 12-faz sırasını yürütür: kontak doğrula → mod seç (Persistence `resolveMode`) → identity → protocol → ECU/PID/DID/firmware keşif → capability → fingerprint → knowledge → evidence(+pattern+persistence checkpoint) → change_detection → report → completed. **Fazların GERÇEK işi enjekte edilen `handlers` ile yapılır** (foundation'da yok → tümü `skipped`); ileride gerçek discovery servisleri buraya BAĞLANIR. **Faz çıktısı** (success/skipped/error/timeout/cancelled) runtime'a yansıtılır: keşifler `recordEcu/Pid/Did/Firmware`, hata `reportPhaseFailure` (KRİTİK identity/protocol→failed; kritik-değil→skip+warn FAIL-SOFT devam). **Fail-soft zinciri:** fingerprint düşse knowledge çalışır · knowledge düşse persistence yazılır · persistence düşse report yine üretilir (runtime.completeScan ÖNCE, persistence.completeScan try/catch SONRA). **Kontak güvenliği:** her aktif faz öncesi Ignition Source'tan `getConfirmedValue()` okunur→runtime'a beslenir; doğrulanmazsa `waiting_for_ignition`'da DURUR (index ilerlemez, aktif sorgu yok), kontak gelince aynı fazdan devam. **Mod:** Persistence'tan tamamlanmış full_scan varsa `change_check`. **Progress** gerçek fazlara göre (runtime PHASE_PROGRESS_FLOOR + ölçülmüş updateProgress), sahte yüzde YOK, 100'de biter. **Event:** scan_started/phase_started/phase_completed/phase_failed/progress_changed/report_ready/scan_completed/scan_cancelled/scan_failed. **YALIN:** fazlar ÇAĞRILABİLİR API (`runNextPhase()` primitif + `run()` sonlu sıra); **timer YOK · yeni thread YOK · sürekli döngü YOK · hot-path YOK · OBD polling'e dokunmaz**; SystemBoot/Assistant/Dashboard wiring YOK; import yan etkisiz (runtime/persistence/ignition yalnız start/run'da kullanılır); yeni bağımlılık yok. Bounded (keşif birikimi ECU128/PID512/DID512/FW128 · listener32 · uyarı16); immutable (snapshot+event deep-freeze); privacy (sanitizeText, ham/VIN/MAC girmez); zero-leak (dispose yalnız kendi dinleyicileri; enjekte deps çağıranındır). Gerçek OBD komutu/PID/DID/Discovery algoritması/native DEĞİŞMEZ. | `platform/deepScan/deepScanOrchestrator.ts` (yeni), `platform/deepScan/index.ts` (export), `__tests__/deepScanOrchestrator.test.ts` (yeni, 21 test) | Gerçek araçta (gerçek discovery handler'ları bağlandıktan SONRA): (1) araç bağlanınca kontak doğrulanır → mod (full/change) seçilir; (2) faz sırası deterministik yürür, her faz tamamlandıkça progress artar (100'de biter); (3) kontak doğrulanmadan aktif sorgu BAŞLAMAZ (`waiting_for_ignition`); (4) tek modül düşse (firmware/fingerprint/knowledge/persistence) Deep Scan ÇÖKMEZ, uygun noktadan devam eder, report yine üretilir; (5) cancel/timeout doğru işlenir; (6) tarama sonu Persistence'a kayıt yazılır (sonraki bağlantı change_check); (7) BASIC_JS/Mali-400'de CPU/RAM regresyonu yok (timer yok, pasifken sıfır yük). ⚠️ **Gerçek discovery/OBD entegrasyonu + SystemBoot wiring + Assistant Context yansıtma SONRAKİ PR KAPSAMI.** Test edilmeden 🟢 yapma. | 2026-07-11 | | 31 | **🔴 Device Validation Pending — Deep Scan Ignition Source Foundation** — Deep Scan Runtime'ın üç durumlu `ignitionConfirmed`'ını GÜVENLİ + deterministik besleyecek çözümleyici katman. ⚠️ **KOD GERÇEĞİ (salt-okunur denetim):** depoda DOĞRUDAN kontak/ACC/key-state yayan HİÇBİR kaynak YOK — VAL `NormalizedVehicleData`, Hiworld CAN protokolü (RPM/BATT_VOLT/GEAR/REVERSE var, ACC/IGN komutu YOK), NativeHAL (AAOS VHAL) ve `useAssistantContextStore` (`status.ignition=null` "sinyal YOK") teyit etti; `commandExecutor.ignitionOn` yalnız remote-command parametresi. `resolveIgnitionState(evidence, now)` SAF çözümleyici: yalnız **authoritative** kaynaklar (`native_acc`/`can_ignition`/`engine_running`/`manual`) onay verir; **yardımcı** kaynaklar (`rpm`/`alternator`/`battery_voltage`/`obd_transport`) tek başına ASLA ON üretmez → `unknown`. **Fail-closed:** kesin doğrulanamazsa `unknown`→`confirmed=null`. Tehlikeli tekil varsayım YOK (hız>0/RPM>0/voltaj/OBD bağlı → ON yasak). **Fizik magic number UYDURULMADI** (kodda motor-çalışıyor RPM eşiği / alternatör voltaj eşiği tanımlı değil → ham sayıya eşik uygulanmaz; yardımcı sinyaller çağıran tarafından bool'a yorumlanıp `engine_running` authoritative kanıtı olur = zero-trust). **Stale/zaman:** her kanıt `observedAt` (Date.now ms, VAL ile aynı taban); yaş>5000ms (VAL SIGNAL_SOURCE_DEFAULTS timeout gerekçesi) veya saat-geri (age<0) → stale, onay için kullanılmaz. **Çelişki:** iki güvenilir authoritative kaynak zıtsa → `unknown` (fail-closed). **Confidence kapısı** 0.7 (VAL base 0.85–0.98 gerekçesi, politika sabiti). **Adapter API:** `getConfirmedValue():boolean\|null`. **YALIN:** `deepScanRuntimeService`'e/SystemBoot'a OTOMATİK BAĞLANMAZ; polling timer YOK (pull `refresh()`/push `submitEvidence()`); import yan etkisiz; yeni bağımlılık yok. Manual override PROD varsayılan KAPALI (dev/test). Bounded (kanıt 16, listener 32); immutable (deep-freeze); privacy (reason `sanitizeText`, VIN/MAC/koordinat/ham/secret snapshot'a girmez); fail-soft (kaynak/listener hatası izole, throw yok). | `platform/deepScan/deepScanIgnitionSource.ts` (yeni), `platform/deepScan/index.ts` (export), `__tests__/deepScanIgnitionSource.test.ts` (yeni, 31 test) | Gerçek araçta (orchestration PR'ı bağlandıktan SONRA): (1) ACC/ON durumu doğru algılanır (gerçek native/CAN ignition kaynağı eklenince); (2) kontak kapalıyken `false` veya `unknown`; (3) RPM tek başına yanlış ON ÜRETMEZ; (4) OBD adaptörü takılı ama kontak kapalıyken ON üretmez; (5) kaynaklar çelişince `unknown`/null; (6) Deep Scan aktif sorguları yalnız doğrulanmış ON ile başlar; (7) BASIC_JS/Mali-400'de CPU/RAM regresyonu yok (saf çözümleyici, timer yok); (8) farklı MCU/CAN kutularında kaynak desteği AYRI raporlanır (şu an hiçbirinde doğrudan kaynak yok → daima `unknown`, fail-closed). ⚠️ **Runtime wiring + gerçek orchestration + doğrudan kaynak entegrasyonu SONRAKİ PR KAPSAMI.** Test edilmeden 🟢 yapma. | 2026-07-11 | diff --git a/src/__tests__/vehicleHal.test.ts b/src/__tests__/vehicleHal.test.ts new file mode 100644 index 00000000..0b83a74d --- /dev/null +++ b/src/__tests__/vehicleHal.test.ts @@ -0,0 +1,333 @@ +/** + * vehicleHal.test.ts — Vehicle HAL Foundation birim testleri. + * + * Kapsam: katalog tohumlama · ingest/snapshot · signal lookup · supported/unsupported · + * stale · confidence · source · tipli erişimciler · identity privacy (ham VIN reddi) · + * getCapability · subscribe/duplicate/izolasyon/bounded · dispose zero-leak · fail-soft · + * immutable (nested) · input mutate edilmiyor · provider pull · import yan etkisiz. + */ + +import { describe, it, expect } from 'vitest'; +import { + createVehicleHal, + VEHICLE_SIGNAL_IDS, + VEHICLE_HAL_STALE_MS_DEFAULT, + MAX_HAL_LISTENERS, + type VehicleHalDeps, +} from '../platform/vehicleHal'; +import halSource from '../platform/vehicleHal/vehicleHal.ts?raw'; + +const NOW = 4_000_000; +function hal(deps: Partial = {}) { + return createVehicleHal({ now: () => NOW, ...deps }); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Katalog / temel + * ════════════════════════════════════════════════════════════════════════ */ + +describe('katalog ve temel', () => { + it('1) katalog tohumlu — tüm sinyaller supported=false başlar', () => { + const h = hal(); + const snap = h.getSnapshot(); + expect(snap.signals.length).toBe(VEHICLE_SIGNAL_IDS.length); + expect(snap.signals.every((s) => s.supported === false && s.value === null)).toBe(true); + }); + + it('2) ingest sinyal — value/source/quality yansır', () => { + const h = hal(); + const s = h.ingestSignal('vehicle.speed', { value: 54, source: 'can', quality: 'high', confidence: 0.9 }); + expect(s?.value).toBe(54); + expect(s?.source).toBe('can'); + expect(s?.supported).toBe(true); + expect(s?.unit).toBe('km/h'); + }); + + it('3) getSignal / hasSignal lookup', () => { + const h = hal(); + h.ingestSignal('vehicle.rpm', { value: 1800, source: 'obd' }); + expect(h.getSignal('vehicle.rpm')?.value).toBe(1800); + expect(h.hasSignal('vehicle.rpm')).toBe(true); + expect(h.hasSignal('vehicle.oil_temp')).toBe(false); // beslenmedi + }); + + it('4) supported — kaynaklı sinyal', () => { + const h = hal(); + h.ingestSignal('vehicle.fuel_level', { value: 60, source: 'native' }); + expect(h.getSignal('vehicle.fuel_level')?.supported).toBe(true); + }); + + it('5) unsupported — source none → supported=false, value null', () => { + const h = hal(); + const s = h.ingestSignal('vehicle.speed', { value: 54, source: 'none' }); + expect(s?.supported).toBe(false); + expect(s?.value).toBeNull(); // kaynaksız değer taşınmaz + }); + + it('6) stale — eski timestamp → stale=true', () => { + const h = hal({ staleMs: 5000 }); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can', timestamp: NOW - 6000 }); + expect(h.getSignal('vehicle.speed')?.stale).toBe(true); + }); + + it('7) saat geri (age<0) → stale', () => { + const h = hal({ staleMs: 5000 }); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can', timestamp: NOW + 10000 }); + expect(h.getSignal('vehicle.speed')?.stale).toBe(true); + }); + + it('8) confidence clamp [0,1]', () => { + const h = hal(); + expect(h.ingestSignal('vehicle.speed', { value: 1, source: 'can', confidence: 5 })?.confidence).toBe(1); + expect(h.ingestSignal('vehicle.rpm', { value: 1, source: 'can', confidence: -3 })?.confidence).toBe(0); + }); + + it('9) source modeli — geçersiz kaynak → none (supported=false)', () => { + const h = hal(); + const s = h.ingestSignal('vehicle.speed', { value: 54, source: 'hiworld' as never }); + expect(s?.source).toBe('none'); + expect(s?.supported).toBe(false); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * Tipli erişimciler + value tip doğrulama + * ════════════════════════════════════════════════════════════════════════ */ + +describe('tipli erişimciler', () => { + it('10) getSpeed/getRPM/getBatteryVoltage — beslenince değer, yoksa null', () => { + const h = hal(); + expect(h.getSpeed()).toBeNull(); + h.ingestSignal('vehicle.speed', { value: 72, source: 'can' }); + h.ingestSignal('vehicle.battery_voltage', { value: 13.8, source: 'obd' }); + expect(h.getSpeed()).toBe(72); + expect(h.getBatteryVoltage()).toBe(13.8); + expect(h.getRPM()).toBeNull(); // beslenmedi + }); + + it('11) getIgnition — kaynak yok → null (fail-closed)', () => { + const h = hal(); + expect(h.getIgnition()).toBeNull(); + h.ingestSignal('vehicle.ignition', { value: true, source: 'native' }); + expect(h.getIgnition()).toBe(true); + }); + + it('12) value tip doğrulama — number sinyale boolean → null; NaN → null', () => { + const h = hal(); + expect(h.ingestSignal('vehicle.speed', { value: true, source: 'can' })?.value).toBeNull(); + expect(h.ingestSignal('vehicle.speed', { value: NaN, source: 'can' })?.value).toBeNull(); + expect(h.ingestSignal('vehicle.reverse', { value: 1, source: 'can' })?.value).toBeNull(); // bool bekler + }); + + it('13) tpms — number[] doğrulama + kopya döner', () => { + const h = hal(); + h.ingestSignal('vehicle.tpms', { value: [220, 225, 218, 230], source: 'can' }); + const t = h.getTpms(); + expect(t).toEqual([220, 225, 218, 230]); + t!.push(999); // dış mutasyon iç durumu bozmaz + expect(h.getTpms()).toEqual([220, 225, 218, 230]); + }); + + it('14) getGear / getDoorState / getReverse', () => { + const h = hal(); + h.ingestSignal('vehicle.gear', { value: 3, source: 'can' }); + h.ingestSignal('vehicle.door_state', { value: false, source: 'can' }); + h.ingestSignal('vehicle.reverse', { value: true, source: 'can' }); + expect(h.getGear()).toBe(3); + expect(h.getDoorState()).toBe(false); + expect(h.getReverse()).toBe(true); + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * Identity / capability + * ════════════════════════════════════════════════════════════════════════ */ + +describe('identity ve capability', () => { + it('15) getVehicleIdentity — fingerprint hash kabul, HAM VIN reddedilir', () => { + const h = hal(); + expect(h.getVehicleIdentity().supported).toBe(false); + h.ingestIdentity({ fingerprintHash: '1HGCM82633A004352', protocol: 'ISO15765' }); // VIN → reddedilir + expect(h.getVehicleIdentity().fingerprintHash).toBeNull(); + h.ingestIdentity({ fingerprintHash: 'a1b2c3d4e5f60718', protocol: 'ISO15765' }); + const id = h.getVehicleIdentity(); + expect(id.fingerprintHash).toBe('a1b2c3d4e5f60718'); + expect(id.supported).toBe(true); + }); + + it('16) identity privacy — ham VIN snapshot/identity\'ye sızmaz', () => { + const h = hal(); + h.ingestIdentity({ fingerprintHash: '1HGCM82633A004352' }); + expect(JSON.stringify(h.getVehicleIdentity())).not.toContain('1HGCM82633A004352'); + }); + + it('17) getCapability — HAL-yerel supported/status', () => { + const h = hal({ staleMs: 5000 }); + expect(h.getCapability('vehicle.speed').status).toBe('unsupported'); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can', timestamp: NOW }); + expect(h.getCapability('vehicle.speed').status).toBe('available'); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can', timestamp: NOW - 6000 }); + expect(h.getCapability('vehicle.speed').status).toBe('degraded'); // stale + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * Abonelik / provider / yaşam döngüsü + * ════════════════════════════════════════════════════════════════════════ */ + +describe('abonelik, provider, yaşam döngüsü', () => { + it('18) subscribe — değişimde çağrılır; değişmezse duplicate event yok', () => { + const h = hal(); + let count = 0; + const unsub = h.subscribe(() => { count++; }); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + expect(count).toBe(1); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); // aynı → değişmedi + expect(count).toBe(1); + unsub(); + h.ingestSignal('vehicle.speed', { value: 60, source: 'can' }); + expect(count).toBe(1); // unsubscribe sonrası çağrılmaz + }); + + it('19) duplicate listener yok', () => { + const h = hal(); + let count = 0; + const fn = () => { count++; }; + h.subscribe(fn); h.subscribe(fn); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + expect(count).toBe(1); + }); + + it('20) listener hatası izole', () => { + const h = hal(); + let good = 0; + h.subscribe(() => { throw new Error('kötü'); }); + h.subscribe(() => { good++; }); + expect(() => h.ingestSignal('vehicle.speed', { value: 54, source: 'can' })).not.toThrow(); + expect(good).toBe(1); + }); + + it('21) listener bounded (MAX)', () => { + const h = hal(); + for (let i = 0; i < MAX_HAL_LISTENERS; i++) h.subscribe(() => { /* */ }); + h.subscribe(() => { /* */ }); + expect(h.listenerCount).toBe(MAX_HAL_LISTENERS); + }); + + it('22) unsubscribe (açık metod)', () => { + const h = hal(); + let count = 0; + const fn = () => { count++; }; + h.subscribe(fn); + h.unsubscribe(fn); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + expect(count).toBe(0); + }); + + it('23) provider pull (refresh) fail-soft — bir provider throw etse diğerleri okunur', () => { + const h = hal({ providers: { + 'vehicle.speed': () => { throw new Error('kaynak patladı'); }, + 'vehicle.rpm': () => ({ value: 900, source: 'obd' }), + } }); + expect(() => h.refresh()).not.toThrow(); + expect(h.getRPM()).toBe(900); + expect(h.getSpeed()).toBeNull(); + }); + + it('24) refresh idempotent', () => { + const h = hal({ providers: { 'vehicle.speed': () => ({ value: 50, source: 'can' }) } }); + const a = h.refresh(); + const b = h.refresh(); + expect(h.getSpeed()).toBe(50); + expect(typeof a.revision).toBe('number'); + expect(typeof b.revision).toBe('number'); + }); + + it('25) reset — sinyaller supported=false\'e döner, identity temizlenir', () => { + const h = hal(); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + h.ingestIdentity({ fingerprintHash: 'a1b2c3d4e5f60718' }); + h.reset(); + expect(h.getSpeed()).toBeNull(); + expect(h.hasSignal('vehicle.speed')).toBe(false); + expect(h.getVehicleIdentity().supported).toBe(false); + }); + + it('26) dispose zero-leak — listener temizlenir, sonrası no-op', () => { + const h = hal(); + h.subscribe(() => { /* */ }); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + h.dispose(); + expect(h.listenerCount).toBe(0); + expect(h.isDisposed).toBe(true); + expect(h.ingestSignal('vehicle.speed', { value: 60, source: 'can' })).toBeNull(); // no-op + }); +}); + +/* ══════════════════════════════════════════════════════════════════════════ + * Immutability / fail-soft / yalıtım + * ════════════════════════════════════════════════════════════════════════ */ + +describe('immutability, fail-soft, yalıtım', () => { + it('27) snapshot immutable (frozen, nested dahil)', () => { + const h = hal(); + h.ingestSignal('vehicle.tpms', { value: [220, 225], source: 'can' }); + const snap = h.getSnapshot(); + expect(Object.isFrozen(snap)).toBe(true); + expect(Object.isFrozen(snap.signals)).toBe(true); + expect(snap.signals.every((s) => Object.isFrozen(s))).toBe(true); + expect(() => { (snap.signals as unknown[]).push({}); }).toThrow(); + }); + + it('28) getSignal çıktısı frozen', () => { + const h = hal(); + h.ingestSignal('vehicle.speed', { value: 54, source: 'can' }); + expect(Object.isFrozen(h.getSignal('vehicle.speed'))).toBe(true); + }); + + it('29) girdi objesi mutate edilmiyor', () => { + const h = hal(); + const input = { value: 54, source: 'can' as const }; + const before = { ...input }; + h.ingestSignal('vehicle.speed', input); + expect(input).toEqual(before); + }); + + it('30) bounded — bilinmeyen sinyal id reddedilir (katalog dışı eklenemez)', () => { + const h = hal(); + expect(h.ingestSignal('vehicle.warp_drive' as never, { value: 1, source: 'can' })).toBeNull(); + expect(h.getSnapshot().signals.length).toBe(VEHICLE_SIGNAL_IDS.length); // katalog sabit + }); + + it('31) public API fail-soft — bozuk girdi throw etmez', () => { + const h = hal(); + expect(() => h.ingestSignal('vehicle.speed', null as never)).not.toThrow(); + expect(() => h.ingest(null as never)).not.toThrow(); + expect(() => h.ingestIdentity(null as never)).not.toThrow(); + expect(h.getSpeed()).toBeNull(); + }); + + it('32) ingest toplu besleme', () => { + const h = hal(); + h.ingest({ + 'vehicle.speed': { value: 30, source: 'can' }, + 'vehicle.rpm': { value: 1200, source: 'obd' }, + }); + expect(h.getSpeed()).toBe(30); + expect(h.getRPM()).toBe(1200); + }); + + it('33) import yan etkisiz — timer/native/donanım probu YOK', () => { + expect(/setInterval|setTimeout/.test(halSource)).toBe(false); + expect(/\bnavigator\.|Capacitor|nativePlugin/.test(halSource)).toBe(false); + }); + + it('34) SystemBoot / UnifiedVehicleStore wiring yok', () => { + expect(/from\s+['"][^'"]*SystemBoot['"]/.test(halSource)).toBe(false); + expect(/^\s*import[\s{]/m.test(halSource)).toBe(false); // hiç import ifadesi yok (bağımsız foundation) + }); + + it('35) stale default eşiği makul', () => { + expect(VEHICLE_HAL_STALE_MS_DEFAULT).toBeGreaterThan(0); + }); +}); diff --git a/src/platform/vehicleHal/index.ts b/src/platform/vehicleHal/index.ts new file mode 100644 index 00000000..61cbd537 --- /dev/null +++ b/src/platform/vehicleHal/index.ts @@ -0,0 +1,28 @@ +/** + * vehicleHal — Vehicle HAL foundation (barrel). + * + * Bu modülü import etmek YAN ETKİ ÜRETMEZ: timer/abonelik/native çağrı/donanım probu + * YOK. Tekil `vehicleHal` yalnız statik sinyal kataloğunu `supported=false` tohumlar. + * Gerçek kaynak wiring (VAL/UnifiedVehicleStore → ingest) · SystemBoot wiring KAPSAM DIŞI. + */ + +export { + VehicleHal, + createVehicleHal, + vehicleHal, + VEHICLE_SIGNAL_IDS, + VEHICLE_HAL_STALE_MS_DEFAULT, + MAX_HAL_LISTENERS, + type VehicleSignalId, + type VehicleSignalSource, + type SignalQuality, + type VehicleSignal, + type VehicleHalSnapshot, + type VehicleIdentity, + type VehicleSignalCapability, + type VehicleSignalInput, + type VehicleIdentityInput, + type VehicleSignalProvider, + type VehicleHalListener, + type VehicleHalDeps, +} from './vehicleHal'; diff --git a/src/platform/vehicleHal/vehicleHal.ts b/src/platform/vehicleHal/vehicleHal.ts new file mode 100644 index 00000000..0ceed4bd --- /dev/null +++ b/src/platform/vehicleHal/vehicleHal.ts @@ -0,0 +1,454 @@ +/** + * vehicleHal — Vehicle Hardware Abstraction Layer (HAL) — FOUNDATION. + * + * AMAÇ: CarOS Pro'daki TÜM modüllerin araçla YALNIZ bu katman üzerinden konuşmasını + * sağlayan tek resmi arayüz. Deep Scan · AI · Navigation · Fleet · Remote · Dashboard + * ve gelecekteki modüller CAN/ELM327/Hiworld/Native katmanlarını BİLMEZ; yalnız + * `vehicle.getSignal(id)` / `getSpeed()` gibi standart API'yi kullanır. + * + * ⚠️ BUGÜNKÜ DURUM (salt-okunur analiz): Araç verisi Native VHAL (`NativeHALAdapter`) · + * CAN (`CanAdapter` → Hiworld/K24/NWD) · OBD (`ObdAdapter` → ELM327) · GPS'ten gelir; + * `SignalNormalizer` → VAL `NormalizedVehicleData` → `VehicleCompute.worker` füzyonu → + * `VehicleState` (SAB/postMessage) → `UnifiedVehicleStore`. Aynı veri ~40 dosyada + * `useUnifiedVehicleStore` + Assistant Context + Battery Service + gauge SAB ile AYRI + * AYRI okunuyor; per-signal quality/confidence/source/stale/supported/unit veren ORTAK + * bir `getSignal` API'si YOK. Vehicle HAL bu boşluğu doldurur. + * + * NE YAPMAZ (bilinçli — bu PR yalnız FOUNDATION'dır): + * - Araç verisi ÜRETMEZ · YORUMLAMAZ (magic sanity eşiği yok, yalnız tip doğrulama). + * - Kaynağı GİZLER: hiçbir tüketici CAN/OBD/Hiworld bilmez. + * - Gerçek kaynak WIRING'i (VAL/UnifiedVehicleStore → HAL.ingest) AYRI PR'dır → bu + * foundation'da enjekte `providers` yoksa TÜM sinyaller `supported=false` döner. + * - SystemBoot wiring YOK · native değişmez · yeni OBD komutu/CAN decoder/PID YOK · + * Deep Scan/Capability Registry davranışı DEĞİŞMEZ. Import YAN ETKİSİZDİR (timer/ + * abonelik/native çağrı/donanım probu yok). + * + * KARAR: Veri yoksa `null` / `supported=false` (fail-closed). Stale sinyal taze + * gösterilmez. ZERO-LEAK: `dispose()` dinleyicileri bırakır. FAIL-SOFT: provider/ + * listener hatası HAL'i çökertmez; public API throw ETMEZ. Bounded · immutable. + */ + +/* ══════════════════════════════════════════════════════════════════════════ + * Tipler + * ════════════════════════════════════════════════════════════════════════ */ + +export type VehicleSignalId = + | 'vehicle.speed' + | 'vehicle.rpm' + | 'vehicle.coolant_temp' + | 'vehicle.oil_temp' + | 'vehicle.fuel_level' + | 'vehicle.battery_voltage' + | 'vehicle.throttle' + | 'vehicle.ambient_temp' + | 'vehicle.odometer' + | 'vehicle.gear' + | 'vehicle.reverse' + | 'vehicle.ignition' + | 'vehicle.door_state' + | 'vehicle.parking_brake' + | 'vehicle.tpms'; + +export type VehicleSignalSource = 'native' | 'can' | 'obd' | 'deep_scan' | 'inferred' | 'none'; + +export type SignalQuality = 'low' | 'medium' | 'high' | 'unknown'; + +export interface VehicleSignal { + readonly id: VehicleSignalId; + readonly value: T | null; + readonly quality: SignalQuality; + readonly confidence: number; + readonly source: VehicleSignalSource; + readonly timestamp: number; + readonly stale: boolean; + readonly unit: string | null; + readonly supported: boolean; +} + +export interface VehicleHalSnapshot { + readonly revision: number; + readonly updatedAt: number; + readonly signals: readonly VehicleSignal[]; +} + +/** Araç kimliği — HAM VIN DEĞİL: yalnız türetilmiş fingerprint hash + protokol. */ +export interface VehicleIdentity { + readonly fingerprintHash: string | null; + readonly protocol: string | null; + readonly supported: boolean; +} + +/** Bir sinyalin capability özeti (HAL-yerel; Capability Registry'nin yerini ALMAZ). */ +export interface VehicleSignalCapability { + readonly id: VehicleSignalId; + readonly supported: boolean; + readonly status: 'available' | 'degraded' | 'unsupported'; +} + +/** ingest/provider girdisi (kısmi; unit/supported HAL tarafından türetilir). */ +export interface VehicleSignalInput { + readonly value?: unknown; + readonly quality?: SignalQuality; + readonly confidence?: number; + readonly source?: VehicleSignalSource; + readonly timestamp?: number; +} + +export interface VehicleIdentityInput { + readonly fingerprintHash?: unknown; + readonly protocol?: unknown; +} + +export type VehicleSignalProvider = () => VehicleSignalInput | null; + +export type VehicleHalListener = (snapshot: VehicleHalSnapshot) => void; + +export interface VehicleHalDeps { + readonly now?: () => number; + readonly staleMs?: number; + readonly providers?: Partial>; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Sinyal kataloğu (birim + değer tipi) — statik, kaynak YOK + * ════════════════════════════════════════════════════════════════════════ */ + +type ValueKind = 'number' | 'boolean' | 'number[]'; + +interface SignalDef { readonly unit: string | null; readonly kind: ValueKind; } + +const SIGNAL_CATALOG: Readonly> = { + 'vehicle.speed': { unit: 'km/h', kind: 'number' }, + 'vehicle.rpm': { unit: 'rpm', kind: 'number' }, + 'vehicle.coolant_temp': { unit: '°C', kind: 'number' }, + 'vehicle.oil_temp': { unit: '°C', kind: 'number' }, + 'vehicle.fuel_level': { unit: '%', kind: 'number' }, + 'vehicle.battery_voltage': { unit: 'V', kind: 'number' }, + 'vehicle.throttle': { unit: '%', kind: 'number' }, + 'vehicle.ambient_temp': { unit: '°C', kind: 'number' }, + 'vehicle.odometer': { unit: 'km', kind: 'number' }, + 'vehicle.gear': { unit: null, kind: 'number' }, + 'vehicle.reverse': { unit: null, kind: 'boolean' }, + 'vehicle.ignition': { unit: null, kind: 'boolean' }, + 'vehicle.door_state': { unit: null, kind: 'boolean' }, + 'vehicle.parking_brake': { unit: null, kind: 'boolean' }, + 'vehicle.tpms': { unit: 'kPa', kind: 'number[]' }, +}; + +export const VEHICLE_SIGNAL_IDS: readonly VehicleSignalId[] = Object.freeze( + Object.keys(SIGNAL_CATALOG) as VehicleSignalId[], +); + +const VALID_SIGNAL_IDS: ReadonlySet = new Set(VEHICLE_SIGNAL_IDS); +const VALID_SOURCES: ReadonlySet = new Set(['native', 'can', 'obd', 'deep_scan', 'inferred', 'none']); +const VALID_QUALITIES: ReadonlySet = new Set(['low', 'medium', 'high', 'unknown']); + +export const VEHICLE_HAL_STALE_MS_DEFAULT = 5000; +export const MAX_HAL_LISTENERS = 32; +const MAX_TPMS_LEN = 6; +const MAX_TEXT_CHARS = 40; + +/* ══════════════════════════════════════════════════════════════════════════ + * Saf yardımcılar + * ════════════════════════════════════════════════════════════════════════ */ + +function _clamp01(v: unknown): number { + if (typeof v !== 'number' || !Number.isFinite(v)) return 0; + if (v < 0) return 0; + if (v > 1) return 1; + return v; +} + +function _quality(v: unknown): SignalQuality { + return typeof v === 'string' && VALID_QUALITIES.has(v) ? (v as SignalQuality) : 'unknown'; +} + +function _source(v: unknown): VehicleSignalSource { + return typeof v === 'string' && VALID_SOURCES.has(v) ? (v as VehicleSignalSource) : 'none'; +} + +/** Tip doğrulama (YORUMLAMA DEĞİL): geçersiz/NaN → null. */ +function _normValue(kind: ValueKind, v: unknown): number | boolean | number[] | null { + if (v === null || v === undefined) return null; + if (kind === 'number') return typeof v === 'number' && Number.isFinite(v) ? v : null; + if (kind === 'boolean') return typeof v === 'boolean' ? v : null; + // number[] + if (!Array.isArray(v)) return null; + const arr: number[] = []; + for (const x of v) { if (arr.length >= MAX_TPMS_LEN) break; if (typeof x === 'number' && Number.isFinite(x)) arr.push(x); } + return arr.length > 0 ? arr : null; +} + +/** Fingerprint hash doğrulaması — HAM VIN reddedilir (17 karakter), yalnız 8–64 hex. */ +function _normFingerprint(v: unknown): string | null { + if (typeof v !== 'string') return null; + const s = v.trim(); + if (s.length === 17) return null; // VIN uzunluğu — asla kabul etme + if (!/^[0-9a-fA-F]{8,64}$/.test(s)) return null; + return s.toLowerCase(); +} + +/** Kısa metin temizliği (protokol adı gibi) — MAC/hex/koordinat sızıntısına karşı bounded. */ +function _text(v: unknown): string | null { + if (typeof v !== 'string') return null; + const s = v.replace(/\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g, '') + .replace(/-?\d{1,3}\.\d{4,}\s*[,;]\s*-?\d{1,3}\.\d{4,}/g, '') + .trim(); + const c = s.length > MAX_TEXT_CHARS ? s.slice(0, MAX_TEXT_CHARS) : s; + return c || null; +} + +function _valueEqual(a: unknown, b: unknown): boolean { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; + } + return a === b; +} + +function _freezeSignal(s: VehicleSignal): VehicleSignal { + return Object.freeze({ ...s, value: Array.isArray(s.value) ? Object.freeze([...s.value]) : s.value }); +} + +/* ══════════════════════════════════════════════════════════════════════════ + * Vehicle HAL + * ════════════════════════════════════════════════════════════════════════ */ + +export class VehicleHal { + private readonly _now: () => number; + private readonly _staleMs: number; + private readonly _providers: Partial>; + + private _signals = new Map(); + private _identity: VehicleIdentity = Object.freeze({ fingerprintHash: null, protocol: null, supported: false }); + private readonly _listeners = new Set(); + private _revision = 0; + private _disposed = false; + + constructor(deps: VehicleHalDeps = {}) { + this._now = typeof deps.now === 'function' ? deps.now : () => Date.now(); + this._staleMs = typeof deps.staleMs === 'number' && deps.staleMs > 0 ? deps.staleMs : VEHICLE_HAL_STALE_MS_DEFAULT; + this._providers = deps.providers ?? {}; + // Katalog tohumla: tüm sinyaller `supported=false`, value null, source none. Donanım probu YOK. + for (const id of VEHICLE_SIGNAL_IDS) this._signals.set(id, this._emptySignal(id)); + } + + /* ── Dahili ──────────────────────────────────────────────────────────── */ + + private _nowSafe(): number { + try { const n = this._now(); return Number.isFinite(n) ? n : 0; } catch { return 0; } + } + + private _emptySignal(id: VehicleSignalId): VehicleSignal { + return _freezeSignal({ + id, value: null, quality: 'unknown', confidence: 0, source: 'none', + timestamp: 0, stale: false, unit: SIGNAL_CATALOG[id].unit, supported: false, + }); + } + + /** Stale hesabı: yaş<0 (saat geri) veya > staleMs → stale. */ + private _isStale(timestamp: number, now: number): boolean { + if (!Number.isFinite(timestamp) || timestamp <= 0) return false; + const age = now - timestamp; + return age < 0 || age > this._staleMs; + } + + private _emit(): void { + if (this._disposed || this._listeners.size === 0) return; + const snap = this.getSnapshot(); + for (const l of [...this._listeners]) { + try { l(snap); } catch (err) { console.error('[VehicleHal] dinleyici hatası — servis etkilenmedi', err); } + } + } + + private _applyInput(id: VehicleSignalId, input: VehicleSignalInput, now: number): boolean { + const def = SIGNAL_CATALOG[id]; + const source = _source(input.source); + const value = source === 'none' ? null : _normValue(def.kind, input.value); + const timestamp = typeof input.timestamp === 'number' && Number.isFinite(input.timestamp) ? input.timestamp : now; + const next: VehicleSignal = _freezeSignal({ + id, + value, + quality: _quality(input.quality), + confidence: _clamp01(input.confidence), + source, + timestamp, + stale: this._isStale(timestamp, now), + unit: def.unit, + supported: source !== 'none', + }); + const prev = this._signals.get(id); + this._signals.set(id, next); + // Değişim: value/source/quality/supported/stale (duplicate event önleme). + return !prev || !_valueEqual(prev.value, next.value) || prev.source !== next.source || + prev.quality !== next.quality || prev.supported !== next.supported || prev.stale !== next.stale; + } + + /* ── Public API ──────────────────────────────────────────────────────── */ + + /** Tek sinyali dışarıdan besler (push). Bilinmeyen id → no-op null. */ + ingestSignal(id: VehicleSignalId, input: VehicleSignalInput): VehicleSignal | null { + if (this._disposed || !VALID_SIGNAL_IDS.has(id) || !input || typeof input !== 'object') return null; + const changed = this._applyInput(id, input, this._nowSafe()); + if (changed) { this._revision++; this._emit(); } + return this._signals.get(id) ?? null; + } + + /** Toplu besleme (push). */ + ingest(signals: Partial>): void { + if (this._disposed || !signals || typeof signals !== 'object') return; + const now = this._nowSafe(); + let changed = false; + for (const id of VEHICLE_SIGNAL_IDS) { + const input = signals[id]; + if (input && typeof input === 'object') { if (this._applyInput(id, input, now)) changed = true; } + } + if (changed) { this._revision++; this._emit(); } + } + + /** Araç kimliği besler (HAM VIN reddedilir). */ + ingestIdentity(input: VehicleIdentityInput): VehicleIdentity { + if (this._disposed || !input || typeof input !== 'object') return this._identity; + const fingerprintHash = _normFingerprint(input.fingerprintHash); + const protocol = _text(input.protocol); + const next: VehicleIdentity = Object.freeze({ fingerprintHash, protocol, supported: fingerprintHash !== null }); + const changed = next.fingerprintHash !== this._identity.fingerprintHash || next.protocol !== this._identity.protocol; + this._identity = next; + if (changed) { this._revision++; this._emit(); } + return next; + } + + /** Sağlayıcılardan çeker (pull, fail-soft) + yeniden hesaplar. İDEMPOTENT. */ + refresh(): VehicleHalSnapshot { + if (this._disposed) return this.getSnapshot(); + const now = this._nowSafe(); + let changed = false; + for (const id of VEHICLE_SIGNAL_IDS) { + const provider = this._providers[id]; + if (typeof provider !== 'function') continue; + let input: VehicleSignalInput | null = null; + try { input = provider(); } catch { input = null; } // kaynak hatası izole + if (input && typeof input === 'object') { if (this._applyInput(id, input, now)) changed = true; } + } + // Stale bayrağını tazele (zaman ilerlemiş olabilir). + for (const id of VEHICLE_SIGNAL_IDS) { + const s = this._signals.get(id)!; + const stale = this._isStale(s.timestamp, now); + if (stale !== s.stale) { this._signals.set(id, _freezeSignal({ ...s, stale })); changed = true; } + } + if (changed) { this._revision++; this._emit(); } + return this.getSnapshot(); + } + + getSnapshot(): VehicleHalSnapshot { + const signals = [...this._signals.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + return Object.freeze({ + revision: this._revision, + updatedAt: this._nowSafe(), + signals: Object.freeze(signals), + }); + } + + getSignal(id: VehicleSignalId): VehicleSignal | null { + return this._signals.get(id) ?? null; // zaten frozen + } + + hasSignal(id: VehicleSignalId): boolean { + return this._signals.get(id)?.supported === true; + } + + /* ── Tipli erişimciler (kaynağı gizler) ──────────────────────────────── */ + + private _num(id: VehicleSignalId): number | null { + const s = this._signals.get(id); + return s && s.supported && typeof s.value === 'number' ? s.value : null; + } + private _bool(id: VehicleSignalId): boolean | null { + const s = this._signals.get(id); + return s && s.supported && typeof s.value === 'boolean' ? s.value : null; + } + + getSpeed(): number | null { return this._num('vehicle.speed'); } + getRPM(): number | null { return this._num('vehicle.rpm'); } + getCoolantTemp(): number | null { return this._num('vehicle.coolant_temp'); } + getOilTemp(): number | null { return this._num('vehicle.oil_temp'); } + getFuelLevel(): number | null { return this._num('vehicle.fuel_level'); } + getBatteryVoltage(): number | null { return this._num('vehicle.battery_voltage'); } + getThrottle(): number | null { return this._num('vehicle.throttle'); } + getAmbientTemp(): number | null { return this._num('vehicle.ambient_temp'); } + getOdometer(): number | null { return this._num('vehicle.odometer'); } + getGear(): number | null { return this._num('vehicle.gear'); } + getReverse(): boolean | null { return this._bool('vehicle.reverse'); } + getIgnition(): boolean | null { return this._bool('vehicle.ignition'); } + getDoorState(): boolean | null { return this._bool('vehicle.door_state'); } + getParkingBrake(): boolean | null { return this._bool('vehicle.parking_brake'); } + getTpms(): number[] | null { + const s = this._signals.get('vehicle.tpms'); + return s && s.supported && Array.isArray(s.value) ? [...s.value] : null; + } + + getVehicleIdentity(): VehicleIdentity { + return this._identity; // frozen; ham VIN içermez + } + + /** HAL-yerel capability özeti (Capability Registry'nin YERİNE geçmez). */ + getCapability(id: VehicleSignalId): VehicleSignalCapability { + const s = this._signals.get(id); + const supported = s?.supported === true; + const status: VehicleSignalCapability['status'] = !supported ? 'unsupported' : s!.stale ? 'degraded' : 'available'; + return Object.freeze({ id, supported, status }); + } + + subscribe(listener: VehicleHalListener): () => void { + if (this._disposed || typeof listener !== 'function') return () => { /* no-op */ }; + if (!this._listeners.has(listener) && this._listeners.size >= MAX_HAL_LISTENERS) return () => { /* no-op */ }; + this._listeners.add(listener); + return () => { this._listeners.delete(listener); }; + } + + /** Açık unsubscribe (subscribe cleanup'ına ek). */ + unsubscribe(listener: VehicleHalListener): void { + this._listeners.delete(listener); + } + + get listenerCount(): number { + return this._listeners.size; + } + + /** Tüm sinyalleri kataloğa (supported=false) döndürür; kimliği temizler. */ + reset(): void { + if (this._disposed) return; + this._signals = new Map(); + for (const id of VEHICLE_SIGNAL_IDS) this._signals.set(id, this._emptySignal(id)); + this._identity = Object.freeze({ fingerprintHash: null, protocol: null, supported: false }); + this._revision++; + this._emit(); + } + + dispose(): void { + if (this._disposed) return; + this._listeners.clear(); + this._signals = new Map(); + this._disposed = true; + } + + get isDisposed(): boolean { + return this._disposed; + } +} + +/** + * Fabrika — DI ile örnek üretir. Yapıcı yalnız statik kataloğu tohumlar (donanım probu/ + * timer/abonelik/native çağrı YOK) → import + oluşturma davranış değiştirmez. + */ +export function createVehicleHal(deps: VehicleHalDeps = {}): VehicleHal { + return new VehicleHal(deps); +} + +/** + * Uygulama geneli tekil HAL. Sağlayıcı YOK (foundation) → gerçek kaynak wiring (VAL/ + * UnifiedVehicleStore → HAL.ingest) gelene kadar tüm sinyaller `supported=false`. + * SystemBoot'a BAĞLI DEĞİLDİR. + */ +export const vehicleHal = new VehicleHal();