Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/__tests__/obdCoreV2.pidExpansion.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
26 changes: 23 additions & 3 deletions src/platform/obd/StandardPidRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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. */
Expand Down
15 changes: 11 additions & 4 deletions src/platform/obd/extendedPidService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand All @@ -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). */
Expand Down
Loading