Skip to content
Open
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
181 changes: 181 additions & 0 deletions src/__tests__/discoveryLiveCapture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* discoveryLiveCapture.test.ts — Canlı discovery servislerinin DiscoveryCaptureService'e
* bağlanması (PR-DISC-2).
*
* Kilitler:
* - extendedPidService: keşif bitmask'inde registry'de OLMAYAN PID → capture; registry'deki
* PID → capture YOK; aralık-bayrağı (00/20/…) → capture YOK; hot-poll/native liste değişmez.
* - didDiscoveryService: POZİTİF DID → capture; NO DATA/7F → capture YOK.
* - manufacturerPidService: profil DID'i "katalog" → setKnownDids ile bilinir → capture YOK.
* - Duplicate filtre, tanı event'i, queue, export.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';

const M = vi.hoisted(() => ({
isNative: true,
pushedLists: [] as string[][],
readObdDidMock: vi.fn(),
}));

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); }),
setObdDiagnosticBurst: vi.fn(async () => {}),
addListener: vi.fn(async () => ({ remove: vi.fn() })),
readObdDid: (...args: unknown[]) => M.readObdDidMock(...args),
},
}));
vi.mock('../platform/crashLogger', () => ({ logError: vi.fn() }));
vi.mock('../platform/safety/vinContext', () => ({ getHandshakeVin: () => null }));

import { discoveryCaptureService } from '../platform/obd/discovery';
import { watchPid, _internals as extInternals } from '../platform/obd/extendedPidService';
import { startDiscovery } from '../platform/obd/didDiscoveryService';
import {
loadProfile, unloadProfile, watchDid, _internals as mfrInternals,
} from '../platform/obd/manufacturerPidService';
import { clear as clearDiag, getEvents } from '../platform/obdDiagnosticRecorder';
import type { VehicleDidProfile } from '../platform/obd/vehicleDidProfile';

const capturedPids = () => discoveryCaptureService.getCaptured().map((r) => r.pidOrDid);

beforeEach(() => {
discoveryCaptureService.reset();
extInternals.reset();
mfrInternals.reset();
clearDiag();
M.isNative = true;
M.pushedLists.length = 0;
M.readObdDidMock.mockReset();
try { localStorage.clear(); } catch { /* jsdom */ }
});

/* ── extendedPidService → PID keşif capture ───────────────────────────────── */

describe('extendedPidService — yeni PID capture', () => {
it('bitmask\'te registry\'de OLMAYAN PID yakalanır; registry\'deki + aralık bayrağı yakalanmaz', () => {
watchPid('5C', () => {}); // keşfi başlat (queue=['00'])
extInternals.onExtendedData({ pid: '00', data: 'BE1FB813' }); // klasik SAE bitmask
const caps = capturedPids();
// '01' ve '03' registry'de yok (enum PID) → yakalanır
expect(caps).toContain('01');
expect(caps).toContain('03');
// '04' (motor yükü) ve '0C' (RPM) registry'de var → yakalanmaz
expect(caps).not.toContain('04');
expect(caps).not.toContain('0C');
// '20' aralık bayrağı (gerçek sinyal değil) → yakalanmaz
expect(caps).not.toContain('20');
});

it('capture, native poll (hot-poll) listesine PID EKLEMEZ', () => {
watchPid('5C', () => {});
extInternals.onExtendedData({ pid: '00', data: 'BE1FB813' });
// Yakalanan keşif PID'leri ('01'/'03') hiçbir native poll listesine sızmamalı.
for (const list of M.pushedLists) {
expect(list).not.toContain('01');
expect(list).not.toContain('03');
}
});

it('aynı keşif iki kez gelince tek capture (duplicate filtre)', () => {
watchPid('5C', () => {});
extInternals.onExtendedData({ pid: '00', data: 'BE1FB813' });
const firstCount = discoveryCaptureService.getCaptured().length;
// Aynı bitmask tekrar (yeni keşif turu) → yeni kayıt olmamalı.
extInternals.onExtendedData({ pid: '00', data: 'BE1FB813' });
expect(discoveryCaptureService.getCaptured().length).toBe(firstCount);
});

it('yeni PID keşfi tanı timeline\'ına \'ecuQuery\' event\'i düşürür', () => {
watchPid('5C', () => {});
extInternals.onExtendedData({ pid: '00', data: 'BE1FB813' });
const evt = getEvents().find((e) => e.stage === 'ecuQuery' && e.technicalMessage.includes('PID'));
expect(evt).toBeDefined();
expect(evt?.status).toBe('info');
});
});

/* ── didDiscoveryService → DID keşif capture ──────────────────────────────── */

describe('didDiscoveryService — DID capture', () => {
it('POZİTİF DID yakalanır; NO DATA / 7F yakalanmaz', async () => {
// 2000 pozitif (supported+data), 2001 desteklenmiyor (NO DATA)
M.readObdDidMock.mockImplementation(async ({ did }: { did: string }) =>
did === '2000' ? { supported: true, data: 'AABBCC' } : { supported: false, data: '' });

const outcome = await startDiscovery({ tx: '7E0', rx: '7E8', from: '2000', to: '2001' });
expect(outcome.summary.positive).toBe(1);

const caps = discoveryCaptureService.getCaptured();
expect(caps.map((r) => r.pidOrDid)).toEqual(['2000']); // yalnız pozitif
const rec = caps[0]!;
expect(rec.discoverySource).toBe('DID');
expect(rec.ecuAddress).toBe('7E8');
expect(rec.mode).toBe('22');
expect(rec.request).toBe('222000');
expect(rec.rawResponse).toBe('AABBCC');
expect(rec.supported).toBe(true);
});

it('discovery başarısız (plugin yok) → capture oluşmaz', async () => {
M.isNative = false; // readObdDid yolu kapalı → plugin_unavailable
const outcome = await startDiscovery({ tx: '7E0', rx: '7E8', from: '2000', to: '2000' });
expect(outcome.summary.stopReason).toBe('plugin_unavailable');
expect(discoveryCaptureService.getCaptured()).toHaveLength(0);
});
});

/* ── manufacturerPidService → katalog (profil) DID'i bilinir ──────────────── */

describe('manufacturerPidService — katalog DID capture edilmez', () => {
const PROFILE: VehicleDidProfile = {
brand: 'Test', source: 'ISO 14229-1 (test fixture)',
ecus: [{ id: 'engine', name: 'Motor ECU', tx: '7E0', rx: '7E8' }],
dids: [
{ did: 'F190', ecu: 'engine', name: 'VIN', unit: '', bytes: 17, min: 0, max: 0, category: 'kimlik', decode: { fn: 'ascii' } },
],
};
const hexOf = (t: string) => [...t].map((c) => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');

it('profildeki DID (F190) okununca capture OLUŞMAZ (katalog = bilinir)', async () => {
loadProfile(PROFILE); // setKnownDids(['F190'])
M.readObdDidMock.mockResolvedValue({ supported: true, data: hexOf('VF1AAAAA000000001') });
const unsub = watchDid('F190', () => {});
await mfrInternals.tick();
unsub();
expect(discoveryCaptureService.getCaptured()).toHaveLength(0);
});

it('profil DID\'i didDiscovery\'de de "bilinir" → yakalanmaz (setKnownDids köprüsü)', async () => {
loadProfile(PROFILE); // F190 katalogda
M.readObdDidMock.mockResolvedValue({ supported: true, data: 'AABB' });
await startDiscovery({ tx: '7E0', rx: '7E8', from: 'F190', to: 'F190' });
// F190 profil (katalog) DID'i → keşif değil.
expect(discoveryCaptureService.getCaptured().map((r) => r.pidOrDid)).not.toContain('F190');
});

it('unloadProfile sonrası aynı DID artık katalog dışı → yakalanır', async () => {
loadProfile(PROFILE);
unloadProfile(); // katalog boşaldı → F190 bilinmiyor
M.readObdDidMock.mockResolvedValue({ supported: true, data: 'AABB' });
await startDiscovery({ tx: '7E0', rx: '7E8', from: 'F190', to: 'F190' });
expect(discoveryCaptureService.getCaptured().map((r) => r.pidOrDid)).toContain('F190');
});
});

/* ── Queue + Export uçtan uca ─────────────────────────────────────────────── */

describe('PR-DISC-2 — queue + export uçtan uca', () => {
it('yakalananlar queue\'ya düşer ve JSON export doğru serileşir', async () => {
M.readObdDidMock.mockResolvedValue({ supported: true, data: 'AA' });
await startDiscovery({ tx: '7E0', rx: '7E8', from: '3000', to: '3001' });

expect(discoveryCaptureService.getCaptured().length).toBe(2); // 3000, 3001
const parsed = JSON.parse(discoveryCaptureService.exportJson());
expect(parsed.count).toBe(2);
expect(parsed.records.map((r: { pidOrDid: string }) => r.pidOrDid).sort()).toEqual(['3000', '3001']);
});
});
17 changes: 16 additions & 1 deletion src/platform/obd/didDiscoveryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Capacitor } from '@capacitor/core';
import { CarLauncher } from '../nativePlugin';
import { logError } from '../crashLogger';
import { discoveryCaptureService } from './discovery';

/** DID'ler arası bekleme (ms) — ECU'yu ardışık sorgularla boğmamak için. */
export const DISCOVERY_INTER_DID_DELAY_MS = 150;
Expand Down Expand Up @@ -131,8 +132,22 @@ export async function startDiscovery(opts: StartDiscoveryOptions): Promise<DidDi
if (r.supported && r.data) {
results.push({ did, dataHex: r.data, bytes: hexToBytes(r.data) });
positive++;
// PR-DISC-2: yalnız POZİTİF (supported && data) DID keşif hattına yakalanır;
// 7F/NO DATA aşağıdaki negatif dalda KAYDEDİLMEZ. DiscoveryCaptureService
// katalogdaki (profil) DID'leri + tekrarları eler. Fail-soft: taramayı etkilemez.
try {
discoveryCaptureService.capture({
discoverySource: 'DID',
mode: '22',
ecuAddress: rx,
pidOrDid: did,
request: `22${did}`,
rawResponse: r.data,
supported: true,
});
} catch (e) { logError('OBD:DiscoveryCaptureDid', e); }
} else {
negative++; // 7F / desteklenmiyor — sayılır, listeye GİRMEZ
negative++; // 7F / desteklenmiyor — sayılır, listeye GİRMEZ, KEŞİF YAKALANMAZ
}
} catch (e) {
// Bağlantı koptu (native reject) — KISMİ sonuçla dürüst dur, hatayı yutma.
Expand Down
20 changes: 20 additions & 0 deletions src/platform/obd/extendedPidService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { PluginListenerHandle } from '@capacitor/core';
import { STANDARD_PID_MAP, decodeStandardPid } from './StandardPidRegistry';
import type { StandardPidDef } from './StandardPidRegistry';
import { logError } from '../crashLogger';
import { discoveryCaptureService } from './discovery';

/** İzlenebilir PID sayısı TS tavanı — rotasyon gecikmesi makul kalsın (16 PID ≈ 16 tur). */
export const ELM_WATCH_CAP = 16;
Expand Down Expand Up @@ -123,6 +124,25 @@ function _onExtendedData(event: { pid: string; data: string }): void {
const found = parseSupportedBitmask(pid, event.data);
if (_supported === null) _supported = new Set<string>();
found.forEach((p) => _supported!.add(p));

// PR-DISC-2: keşif bitmask'inde araç DESTEKLİYOR ama registry'de OLMAYAN standart
// PID'leri yakala (aralık-bayrağı PID'leri 00/20/40/… gerçek sinyal değil → atla;
// registry'dekiler zaten bilinir → DiscoveryCaptureService onları da eler). Fail-soft:
// yakalama hatası keşfi ETKİLEMEZ. Hot-poll / native listeye DOKUNMAZ (yalnız kayıt).
for (const foundPid of found) {
if ((DISCOVERY_PIDS as readonly string[]).includes(foundPid)) continue;
if (STANDARD_PID_MAP.has(foundPid)) continue;
try {
discoveryCaptureService.capture({
discoverySource: 'PID',
mode: '01',
pidOrDid: foundPid,
request: `01${pid}`, // hangi supported-bitmask sorgusundan bulundu
rawResponse: event.data,
supported: true, // bitmask "destekleniyor" diyor
});
} catch (e) { logError('OBD:DiscoveryCapturePid', e); }
}
// Zincir: bir sonraki aralığın bitmask PID'i destekliyse kuyruğa ekle.
const idx = DISCOVERY_PIDS.indexOf(pid as (typeof DISCOVERY_PIDS)[number]);
const next = idx >= 0 ? DISCOVERY_PIDS[idx + 1] : undefined;
Expand Down
21 changes: 21 additions & 0 deletions src/platform/obd/manufacturerPidService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
decodeCompiledDid,
} from './vehicleDidProfile';
import type { CompiledDidDef, VehicleDidValue } from './vehicleDidProfile';
import { discoveryCaptureService } from './discovery';

/** Round-robin zamanlayıcı aralığı (ms) — üretici verileri yavaş değişir, 2-5s yeter. */
export const MANUFACTURER_POLL_INTERVAL_MS = 3000;
Expand Down Expand Up @@ -59,6 +60,8 @@ export function loadProfile(rawProfile: unknown): { ok: true } | { ok: false; er
const result = validateVehicleDidProfile(rawProfile);
if (!result.valid) return { ok: false, errors: result.errors };
_profile = compileVehicleDidProfile(result.profile);
// PR-DISC-2: profildeki DID'ler = "katalog" → keşif yakalayıcı bunları "yeni" saymasın.
discoveryCaptureService.setKnownDids(_profile.keys());
_unsupported.clear();
_values.clear();
_rrIndex = 0;
Expand All @@ -69,6 +72,7 @@ export function loadProfile(rawProfile: unknown): { ok: true } | { ok: false; er
/** Profili kaldırır — izleyici kalmışsa bile zamanlayıcı durur (profilsiz okunacak DID yok). */
export function unloadProfile(): void {
_profile = null;
discoveryCaptureService.setKnownDids([]); // katalog boşaldı
_unsupported.clear();
_values.clear();
_syncTimer();
Expand Down Expand Up @@ -125,6 +129,23 @@ async function _tick(): Promise<void> {
// NaN yalnız sayısal daldan gelir (metin dalı boş string yerine NaN döner) — type guard
// `typeof value === 'string'` durumunda Number.isNaN çağrısını atlar (TS + doğruluk).
if (typeof value === 'number' && Number.isNaN(value)) return; // sınır dışı/bozuk — sessizce atla

// PR-DISC-2: manufacturer çözümlemesinde KATALOG DIŞI DID varsa keşif hattına düşür.
// Profildeki (katalog) DID'ler setKnownDids ile "bilinir" işaretli → DiscoveryCaptureService
// onları ELER (tekrar kaydetmez); yalnız katalog dışı bir DID okunursa yakalanır. Fail-soft.
try {
discoveryCaptureService.capture({
discoverySource: 'DID',
mode: '22',
ecuAddress: def.rx,
pidOrDid: def.did,
request: `22${def.did}`,
rawResponse: r.data,
supported: true,
decodedValue: value,
});
} catch (e) { logError('OBD:DiscoveryCaptureManufacturerDid', e); }

const entry: ManufacturerDidValue = { value, def, updatedAt: Date.now() };
_values.set(did, entry);
_watchers.get(did)?.forEach((cb) => {
Expand Down
Loading