From ee943bf06d86c839dd552033d0dba22672ded44e Mon Sep 17 00:00:00 2001 From: Selim Date: Thu, 9 Jul 2026 17:33:55 +0300 Subject: [PATCH] =?UTF-8?q?feat(obd):=20standart=20J1979=20dizel/emisyon?= =?UTF-8?q?=20PID=20geni=C5=9Flemesi=20(PR-PID-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DPF/dizel telemetrisi için StandardPidRegistry'ye 6 güvenilir standart (SAE J1979-2 Service 01) skaler PID eklenir ve keşif aralığı 0x80/0xA0/0xC0/0xE0 bloklarına genişletilir. Native katmana / hot-poll'a / poll frekansına DOKUNULMADI. Registry (76 → 82 PID, hepsi non-core → yalnız EXTENDED keşifte): - 0x6B EGR sıcaklığı (Banka 1) [destek-baytı + B−40] - 0x78 Egzoz gazı sıcaklığı (EGT Banka 1) [destek-baytı + (256B+C)/10−40] - 0x79 Egzoz gazı sıcaklığı (EGT Banka 2) - 0x7C DPF sıcaklığı (Banka 1) - 0x83 NOx konsantrasyonu (Sensör 1) [ppm] - 0x8E Motor sürtünme torku [A−125] Discovery (extendedPidService): DISCOVERY_PIDS 00/20/40/60 → +80/A0/C0/E0. Zincir yalnız önceki bitmask sonraki bayrağı set ettiyse ilerler (Mali-400: desteklenmeyen blok için boşa sorgu yok). DÜRÜSTLÜK (CLAUDE.md — uydurma yasağı, yalnız güvenilir standart): - 0x6B standartta EGR sıcaklığıdır — "DPF fark basıncı" DEĞİL; doğru adla eklendi. - 0x8B "Diesel Aftertreatment" (bit/durum) ve DPF fark basıncı 0x7A/0x7B iç yapı belirsizliği nedeniyle KASITLI dışarıda bırakıldı (sahada doğrulanınca eklenecek). - DPF kurum/yük yüzdesi standart J1979 skaları değil (üretici-özel Renault DID gerektirir → görev kapsamı dışı); yerine güvenilir 0x7C DPF sıcaklığı eklendi. Test src/__tests__/obdCoreV2.pidExpansion.test.ts (11 kilit): - PID sayısı arttı (≥80) + yeni PID'ler 0x64-0x9D aralığında, doğru decode - hot-poll (core) kümesi birebir DEĞİŞMEDİ (tam 7 core; yeniler non-core) - keşif zinciri 60→80→A0→C0→E0 ilerliyor + desteklenmeyende erken durur - bilinmeyen/eklenmemiş PID (0x8B/0x9D) → NaN (fallback korunuyor) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01ABwbnF4mBZ8Pz4NisRibVE --- src/__tests__/obdCoreV2.pidExpansion.test.ts | 146 +++++++++++++++++++ src/platform/obd/StandardPidRegistry.ts | 26 +++- src/platform/obd/extendedPidService.ts | 15 +- 3 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 src/__tests__/obdCoreV2.pidExpansion.test.ts diff --git a/src/__tests__/obdCoreV2.pidExpansion.test.ts b/src/__tests__/obdCoreV2.pidExpansion.test.ts new file mode 100644 index 00000000..1c9ba3c5 --- /dev/null +++ b/src/__tests__/obdCoreV2.pidExpansion.test.ts @@ -0,0 +1,146 @@ +/** + * obdCoreV2.pidExpansion.test.ts — Standart J1979 dizel/emisyon PID genişlemesi (PR-PID-1). + * + * Kilitlenen davranışlar: + * 1. Registry PID sayısı arttı (yeni dizel/emisyon PID'leri eklendi). + * 2. Yeni PID'ler (0x6B/0x78/0x79/0x7C/0x83/0x8E) doğru ad/birim/decode ile çözülüyor. + * 3. Hot-poll (core) listesi DEĞİŞMEDİ — yalnız 7 çekirdek PID core; yenilerin hiçbiri + * core değil (EXTENDED aday listesinde). Poll frekansına dokunulmadı. + * 4. Keşif aralığı 0x80/0xA0/0xC0/0xE0 bloklarını görüyor (zincir E0'a kadar ilerliyor). + * 5. Bilinmeyen PID davranışı bozulmadı (tanımsız → NaN; destek filtresi sağlam). + */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + STANDARD_PID_MAP, + STANDARD_PIDS, + EXTENDED_CANDIDATE_PIDS, + decodeStandardPid, +} from '../platform/obd/StandardPidRegistry'; + +/** PR-PID-1 ile eklenen yeni standart PID'ler. */ +const NEW_PIDS = ['6B', '78', '79', '7C', '83', '8E'] as const; +/** Değişmemesi gereken hot-poll (core) kümesi. */ +const CORE_PIDS = ['05', '0B', '0C', '0D', '0F', '11', '2F'] as const; + +describe('PR-PID-1 — registry genişlemesi', () => { + it('1) PID sayısı arttı (≥ 80, önceki 76 + yeni dizel/emisyon)', () => { + expect(STANDARD_PIDS.length).toBeGreaterThanOrEqual(80); + expect(STANDARD_PID_MAP.size).toBe(STANDARD_PIDS.length); + }); + + it('2) yeni PID\'lerin hepsi kayıtlı ve 0x64-0x9D aralığında', () => { + for (const pid of NEW_PIDS) { + const def = STANDARD_PID_MAP.get(pid); + expect(def, `${pid} kayıtlı olmalı`).toBeDefined(); + const n = parseInt(pid, 16); + expect(n).toBeGreaterThanOrEqual(0x64); + expect(n).toBeLessThanOrEqual(0x9d); + } + }); + + it('3) EGT (0x78) destek-baytı sonrası sensör 1: (256B+C)/10−40', () => { + // A=01 (destek), B,C = 0x0D4C → (3404)/10−40 = 300.4°C + expect(decodeStandardPid('78', '010D4C')).toBeCloseTo(300.4, 5); + expect(STANDARD_PID_MAP.get('78')?.unit).toBe('°C'); + }); + + it('4) EGR sıcaklığı (0x6B) destek-baytı sonrası B−40', () => { + // A=01 (destek), B=0x64 → 100−40 = 60°C + expect(decodeStandardPid('6B', '0164')).toBe(60); + }); + + it('5) DPF sıcaklığı (0x7C) + NOx (0x83) + sürtünme torku (0x8E)', () => { + expect(decodeStandardPid('7C', '010D4C')).toBeCloseTo(300.4, 5); // DPF sıcaklık + expect(decodeStandardPid('83', '01012C')).toBe(300); // NOx = 0x012C = 300 ppm + expect(decodeStandardPid('8E', 'FF')).toBe(130); // sürtünme torku A−125 + }); + + it('6) her yeni PID min/max aralık koruması sağlam (FF..FF → aralık içi/NaN)', () => { + for (const pid of NEW_PIDS) { + const def = STANDARD_PID_MAP.get(pid)!; + const v = decodeStandardPid(pid, 'FF'.repeat(def.bytes)); + if (!Number.isNaN(v)) { + expect(v).toBeGreaterThanOrEqual(def.min); + expect(v).toBeLessThanOrEqual(def.max); + } + } + }); +}); + +describe('PR-PID-1 — hot-poll (core) DEĞİŞMEDİ', () => { + it('7) tam olarak 7 core PID var ve kümesi birebir aynı', () => { + const coreInRegistry = STANDARD_PIDS.filter((d) => d.core).map((d) => d.pid).sort(); + expect(coreInRegistry).toEqual([...CORE_PIDS].sort()); + }); + + it('8) yeni PID\'lerin HİÇBİRİ core değil (hepsi EXTENDED adayı)', () => { + for (const pid of NEW_PIDS) { + expect(STANDARD_PID_MAP.get(pid)?.core).toBeFalsy(); + expect(EXTENDED_CANDIDATE_PIDS).toContain(pid); + } + }); +}); + +describe('PR-PID-1 — bilinmeyen PID davranışı korunuyor', () => { + it('9) tanımsız PID → NaN (fallback bozulmadı)', () => { + expect(decodeStandardPid('FF', '00')).toBeNaN(); + expect(decodeStandardPid('9D', '00')).toBeNaN(); // aralıkta ama eklenmedi → tanımsız + expect(decodeStandardPid('8B', '0000')).toBeNaN(); // kasıtlı eklenmedi → tanımsız + }); +}); + +/* ── Keşif aralığı genişlemesi (extendedPidService) ─────────────────────────── */ + +const M = vi.hoisted(() => ({ pushedLists: [] as string[][], isNative: true })); +vi.mock('@capacitor/core', () => ({ + Capacitor: { isNativePlatform: vi.fn(() => M.isNative) }, +})); +vi.mock('../platform/nativePlugin', () => ({ + CarLauncher: { + setObdExtendedPids: vi.fn(async (opts: { pids: string[] }) => { M.pushedLists.push(opts.pids); }), + addListener: vi.fn(async () => ({ remove: vi.fn() })), + }, +})); +vi.mock('../platform/crashLogger', () => ({ logError: vi.fn() })); + +import { watchPid, _internals } from '../platform/obd/extendedPidService'; + +describe('PR-PID-1 — keşif 0x80/A0/C0/E0 bloklarını görüyor', () => { + beforeEach(() => { + _internals.reset(); + M.pushedLists.length = 0; + M.isNative = true; + }); + + it('10) zincir 60→80→A0→C0→E0 ilerliyor (her bitmask sonrakini destekliyorsa)', () => { + watchPid('78', () => {}); // yeni EGT PID'ini izle + // Her bitmask yanıtı yalnız bir sonraki 0x_0 bayrağını set eder (byte3 bit0). + const NEXT = '00000001'; + _internals.onExtendedData({ pid: '00', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['20']); + _internals.onExtendedData({ pid: '20', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['40']); + _internals.onExtendedData({ pid: '40', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['60']); + _internals.onExtendedData({ pid: '60', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['80']); // ← YENİ blok + _internals.onExtendedData({ pid: '80', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['A0']); // ← YENİ blok + _internals.onExtendedData({ pid: 'A0', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['C0']); // ← YENİ blok + _internals.onExtendedData({ pid: 'C0', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual(['E0']); // ← YENİ blok + _internals.onExtendedData({ pid: 'E0', data: NEXT }); + expect(_internals.getDiscoveryQueue()).toEqual([]); // E0 sonrası zincir biter + }); + + it('11) blok desteklenmezse zincir erken durur (Mali-400: boşa sorgu yok)', () => { + watchPid('78', () => {}); + // 60 yanıtı 0x80 bayrağını set ETMEZSE zincir 60\'ta durur. + _internals.onExtendedData({ pid: '00', data: '00000001' }); + _internals.onExtendedData({ pid: '20', data: '00000001' }); + _internals.onExtendedData({ pid: '40', data: '00000001' }); + _internals.onExtendedData({ pid: '60', data: '00000000' }); // 0x80 yok + expect(_internals.getDiscoveryQueue()).toEqual([]); + }); +}); diff --git a/src/platform/obd/StandardPidRegistry.ts b/src/platform/obd/StandardPidRegistry.ts index 0181d415..9d7069b1 100644 --- a/src/platform/obd/StandardPidRegistry.ts +++ b/src/platform/obd/StandardPidRegistry.ts @@ -5,9 +5,13 @@ * uygulamadan liste/formül alınmadı — ticari lisans kuralı (CLAUDE.md) gereği. * * KAPSAM v1: formülü tanımlı SAYISAL PID'ler (~55 adet). Bit/enum kodlu durum PID'leri - * (0x03 yakıt sistemi durumu, 0x12, 0x13, 0x1C-0x1E…) ve çok-sensörlü destek-baytlı - * PID'ler (0x66+) bilinçli olarak v2'ye bırakıldı — yanlış çözümleme riski taşımadan - * önce sayısal çekirdek sahada doğrulansın. + * (0x03 yakıt sistemi durumu, 0x12, 0x13, 0x1C-0x1E…) StandardPidEnums'ta ayrı çözülür. + * + * KAPSAM v2 (PR-PID-1): 0x64-0x98 dizel/egzoz/emisyon aralığından yalnız formülü kamu + * standardında NET olan skaler PID'ler eklendi (EGR/EGT/DPF sıcaklığı, NOx, sürtünme + * torku). Bu PID'lerin çoğu ilk baytta bir "veri destek" bitmask'i taşır; decode İLK + * sensörü (A'dan sonraki baytlar) çözer. İç yapısı belirsiz olanlar (DPF fark basıncı + * 0x7A/0x7B, 0x8B Diesel Aftertreatment bit-durum) sahada doğrulanana dek DIŞARIDA. * * TASARIM: veri-güdümlü — her PID bir kayıt: ham veri baytları → değer saf fonksiyonla * çözülür, sınır dışı NaN döner (çağıran atlar, paket düşmez — obdSanitizer felsefesi). @@ -130,6 +134,22 @@ const DEFS: StandardPidDef[] = [ { pid: '61', name: 'Sürücü talep torku', unit: '%', bytes: 1, min: -125, max: 130, category: 'tork', decode: (b) => A(b) - 125 }, { pid: '62', name: 'Gerçek motor torku', unit: '%', bytes: 1, min: -125, max: 130, category: 'tork', decode: (b) => A(b) - 125 }, { pid: '63', name: 'Referans motor torku', unit: 'Nm', bytes: 2, min: 0, max: 65535, category: 'tork', decode: AB }, + + /* ── v2 dizel / egzoz / emisyon genişlemesi (PR-PID-1) ────────────────────── + * SAE J1979-2 Service 01 0x64-0x98 aralığı. Bu PID'lerin ÇOĞU ilk baytta bir + * "veri destek / mevcudiyet" bitmask'i (A) taşır; sensör değerleri onu izler. + * Bu yüzden decode İLK sensörü (A'dan SONRAKİ baytlar) çözer — A atlanır. + * Yalnızca formülü kamu standardında NET olan skaler PID'ler eklendi; DPF fark + * basıncı (0x7A/0x7B iç yapı belirsizliği) ve 0x8B "Diesel Aftertreatment" + * (bit/durum) sahada doğrulanana dek KASITLI dışarıda — uydurma çözümleme yok. + * Not: 0x6B standartta EGR sıcaklığıdır (DPF fark basıncı DEĞİL). */ + // egtTemp: [A=destek] B,C = sensör 1 sıcaklığı = (256B+C)/10 − 40 (EGT/DPF sıcaklık ölçeği) + { pid: '6B', name: 'EGR sıcaklığı (Banka 1)', unit: '°C', bytes: 2, min: -40, max: 215, category: 'emisyon', decode: (b) => b[1]! - 40 }, + { pid: '78', name: 'Egzoz gazı sıcaklığı (EGT B1)', unit: '°C', bytes: 3, min: -40, max: 6513.5, category: 'sicaklik', decode: (b) => (b[1]! * 256 + b[2]!) / 10 - 40 }, + { pid: '79', name: 'Egzoz gazı sıcaklığı (EGT B2)', unit: '°C', bytes: 3, min: -40, max: 6513.5, category: 'sicaklik', decode: (b) => (b[1]! * 256 + b[2]!) / 10 - 40 }, + { pid: '7C', name: 'DPF sıcaklığı (Banka 1)', unit: '°C', bytes: 3, min: -40, max: 6513.5, category: 'sicaklik', decode: (b) => (b[1]! * 256 + b[2]!) / 10 - 40 }, + { pid: '83', name: 'NOx konsantrasyonu (Sensör 1)', unit: 'ppm', bytes: 3, min: 0, max: 65535, category: 'emisyon', decode: (b) => b[1]! * 256 + b[2]! }, + { pid: '8E', name: 'Motor sürtünme torku', unit: '%', bytes: 1, min: -125, max: 130, category: 'tork', decode: (b) => A(b) - 125 }, ]; /** PID ('04') → tanım. Büyük-harf 2 hane hex anahtar. */ diff --git a/src/platform/obd/extendedPidService.ts b/src/platform/obd/extendedPidService.ts index 02955879..412dc908 100644 --- a/src/platform/obd/extendedPidService.ts +++ b/src/platform/obd/extendedPidService.ts @@ -13,8 +13,9 @@ * - İzlenen liste TS tavanı ELM_WATCH_CAP (16) ile sınırlı (native tavanı 32 ayrıca var). * - Değer bildirimleri zaten ≤1 olay/poll-turu — UI thread'e yük binmez. * - * KEŞİF: SAE J1979 Mode 01 PID 00/20/40/60 bitmask'leri AYNI extended kanaldan okunur - * (ekstra native API yok). Zincirleme: 00 → (0x20 destekliyse) 20 → 40 → 60. + * KEŞİF: SAE J1979 Mode 01 PID 00/20/40/60/80/A0/C0/E0 bitmask'leri AYNI extended + * kanaldan okunur (ekstra native API yok). Zincirleme: 00 → (destekliyse) 20 → 40 → + * 60 → 80 → A0 → C0 → E0; her adım yalnız önceki bitmask sonraki bayrağı set ettiyse. * Keşif tamamlanınca desteklenmeyen izlenen PID'ler native listeden çıkarılır * (ELM327'de her desteklenmeyen sorgu ~200ms NO-DATA bekletir — obdPidConfig dersi). */ @@ -32,8 +33,14 @@ export const ELM_WATCH_CAP = 16; /** BURST modu (Canlı Test) tavanı — tüm çekirdek-olmayan PID'ler izlenebilsin. */ export const ELM_WATCH_CAP_BURST = 48; -/** Bitmask keşif PID'leri — sırayla zincirlenir. */ -const DISCOVERY_PIDS = ['00', '20', '40', '60'] as const; +/** + * Bitmask keşif PID'leri — sırayla zincirlenir. Her 0x_0 PID, bir sonraki 32'lik + * aralığın "desteklenen-PID" bayrağıdır; zincir yalnız bir önceki bitmask o bayrağı + * set ettiyse ilerler (araç desteklemiyorsa boşa sorgu yok — Mali-400 kuralı). + * PR-PID-1: dizel/emisyon PID'leri (0x64-0x98) 0x60/0x80 bloklarında yaşadığından + * keşif aralığı 0x80/0xA0/0xC0/0xE0 bloklarına genişletildi. + */ +const DISCOVERY_PIDS = ['00', '20', '40', '60', '80', 'A0', 'C0', 'E0'] as const; export interface ExtendedPidValue { /** Çözülmüş fiziksel değer (valid=false ise NaN). */