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
47 changes: 5 additions & 42 deletions frontend/src/plugins/waveforge/IncrementalClassifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
detectClockDataLinks,
detectSignatures,
scoreProtocolProperties,
groupChannelsByLinks,
classifyChannelRoles,
} from "./laDecoders";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -183,6 +185,7 @@ export class IncrementalClassifier {
const clockDataLinks = detectClockDataLinks(clockProfiles, dataProfiles, this.allEdges);

const signatures = detectSignatures(channels, this.allEdges, this.sampleBuffer, this.rate, this.width);
const roles = classifyChannelRoles(channels, clockDataLinks, this.allEdges);

return {
edges: this.allEdges,
Expand All @@ -192,52 +195,12 @@ export class IncrementalClassifier {
dataChannels,
clockDataLinks,
signatures,
roles,
};
}

/**
* Group active channels into independent clusters.
* Channels linked by clock-data pairs form one group.
* Unlinked async channels each form their own group.
*/
private groupChannels(profile: SignalProfile): number[][] {
const active = profile.activeChannels;
if (active.length === 0) return [];

// Union-Find for grouping
const parent = new Map<number, number>();
for (const ch of active) parent.set(ch, ch);
const find = (x: number): number => {
let root = x;
while (parent.get(root)! !== root) root = parent.get(root)!;
// Path compression
let curr = x;
while (curr !== root) {
const next = parent.get(curr)!;
parent.set(curr, root);
curr = next;
}
return root;
};
const union = (a: number, b: number) => {
const ra = find(a), rb = find(b);
if (ra !== rb) parent.set(ra, rb);
};

// Link clock-data pairs
for (const link of profile.clockDataLinks) {
union(link.clock, link.data);
}

// Collect groups
const groupMap = new Map<number, number[]>();
for (const ch of active) {
const root = find(ch);
if (!groupMap.has(root)) groupMap.set(root, []);
groupMap.get(root)!.push(ch);
}

return Array.from(groupMap.values()).map(g => g.sort((a, b) => a - b));
return groupChannelsByLinks(profile);
}

/**
Expand Down
44 changes: 36 additions & 8 deletions frontend/src/plugins/waveforge/LaDecodersPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useMemo, useEffect, useCallback } from "react";
import { autoDetectLaProtocol } from "./laDecoders";
import { autoDetectLaProtocolMulti } from "./laDecoders";
import type { DecoderTab, LaCapture, AutoDetectResult, TickerRow, I2CFrame, SPIFrame, CANFrame, OneWireFrame, SWDFrame, JTAGFrame, ModbusFrame, LINFrame } from "./laDecoders";
import { DEFAULT_BAUDS, DEFAULT_LABEL_WIDTH } from "./laChannelConfig";

Expand Down Expand Up @@ -63,7 +63,7 @@ export function LaDecodersPanel({ frozenBuf, sampleRate, capture, panSample, zoo
const [jtag, setJtag] = useState<JtagConfig>(defaultJtag ?? { tck: 0, tms: 1, tdi: 2, tdo: 3 });
const [modbus, setModbus] = useState<ModbusConfig>(defaultModbus ?? { ch: 0, baud: 9600, parity: "even" });
const [lin, setLin] = useState<LinConfig>(defaultLin ?? { ch: 0, baud: 19200, checksum: "classic" });
const [lastAutoDetect, setLastAutoDetect] = useState<AutoDetectResult | null>(null);
const [lastAutoDetect, setLastAutoDetect] = useState<AutoDetectResult[] | null>(null);

useEffect(() => { if (defaultUart) setUart(defaultUart); }, [defaultUart]);
useEffect(() => { if (defaultI2c) setI2c(defaultI2c); }, [defaultI2c]);
Expand All @@ -82,8 +82,26 @@ export function LaDecodersPanel({ frozenBuf, sampleRate, capture, panSample, zoo
const handleAutoDetect = useCallback(() => {
if (!effectiveBuf || effectiveBuf.length === 0 || effectiveRate <= 0) return;
const cap = capture ?? { samples: effectiveBuf, sampleRate: effectiveRate, width: 8, source: "live", importedAt: new Date().toISOString() };
const result = autoDetectLaProtocol(cap);
setLastAutoDetect(result);
const results = autoDetectLaProtocolMulti(cap);
setLastAutoDetect(results);
// Apply the top result
const result = results[0];
if (result) {
const patch: Partial<{ tab: DecoderTab; uart: UartConfig; i2c: I2cConfig; spi: SpiConfig; can: CanConfig; onewire: OneWireConfig; swd: SwdConfig; jtag: JtagConfig; modbus: ModbusConfig; lin: LinConfig }> = { tab: result.tab };
if (result.uart) patch.uart = result.uart;
if (result.i2c) patch.i2c = result.i2c;
if (result.spi) patch.spi = { ...result.spi, csActiveLow: spi.csActiveLow };
if (result.can) patch.can = result.can;
if (result.onewire) patch.onewire = result.onewire;
if (result.swd) patch.swd = result.swd;
if (result.jtag) patch.jtag = result.jtag;
if (result.modbus) patch.modbus = result.modbus;
if (result.lin) patch.lin = result.lin;
handleChange(patch);
}
}, [effectiveBuf, effectiveRate, capture, handleChange, spi.csActiveLow]);

const applyAutoDetectResult = useCallback((result: AutoDetectResult) => {
const patch: Partial<{ tab: DecoderTab; uart: UartConfig; i2c: I2cConfig; spi: SpiConfig; can: CanConfig; onewire: OneWireConfig; swd: SwdConfig; jtag: JtagConfig; modbus: ModbusConfig; lin: LinConfig }> = { tab: result.tab };
if (result.uart) patch.uart = result.uart;
if (result.i2c) patch.i2c = result.i2c;
Expand All @@ -95,7 +113,7 @@ export function LaDecodersPanel({ frozenBuf, sampleRate, capture, panSample, zoo
if (result.modbus) patch.modbus = result.modbus;
if (result.lin) patch.lin = result.lin;
handleChange(patch);
}, [effectiveBuf, effectiveRate, capture, handleChange, spi.csActiveLow]);
}, [handleChange, spi.csActiveLow]);

const stripRows = useMemo<TicketItem[]>(() => {
const pan = panSample ?? 0;
Expand Down Expand Up @@ -185,9 +203,19 @@ export function LaDecodersPanel({ frozenBuf, sampleRate, capture, panSample, zoo
Auto
</button>
</div>
{lastAutoDetect && (
<div className="px-2 py-0.5 text-[10px] text-fob-text-dim border-b border-fob-border bg-fob-surface shrink-0">
Auto-detect: {lastAutoDetect.reason} (score {lastAutoDetect.score})
{lastAutoDetect && lastAutoDetect.length > 0 && (
<div className="px-2 py-0.5 text-[10px] text-fob-text-dim border-b border-fob-border bg-fob-surface shrink-0 flex flex-wrap items-center gap-1">
<span>Auto-detect:</span>
{lastAutoDetect.map((r, i) => (
<button
key={i}
onClick={() => applyAutoDetectResult(r)}
className={`px-1.5 py-0.5 rounded-full font-bold uppercase border transition-colors ${r.tab === tab ? "bg-fob-orange text-fob-accent-text border-fob-orange" : "bg-fob-surface border-fob-border hover:bg-fob-border text-fob-text-dim"}`}
title={r.reason}
>
{r.tab} ({r.score})
</button>
))}
</div>
)}

Expand Down
61 changes: 61 additions & 0 deletions frontend/src/plugins/waveforge/laDecoders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
MAX_TICKER,
inferDecoderDefaults,
autoDetectLaProtocol,
autoDetectLaProtocolMulti,
findNearestI2CFrame,
computeEdgeList,
classifySignal,
Expand Down Expand Up @@ -486,6 +487,66 @@ describe("autoDetectLaProtocol", () => {
});
});

describe("autoDetectLaProtocolMulti", () => {
const cap = (samples: Uint8Array, rate: number, labels?: string[]): LaCapture => ({
sampleRate: rate, width: 8, samples, source: "live", importedAt: new Date().toISOString(),
channelLabels: labels,
});

it("returns empty array for empty signal", () => {
const results = autoDetectLaProtocolMulti(cap(new Uint8Array(100), 1_000_000));
expect(results.length).toBe(0);
});

it("returns single result for UART-only signal", () => {
const rate = 1_152_000;
const samples = buildUartSignal([0x48, 0x65, 0x6c], 10, 2);
const results = autoDetectLaProtocolMulti(cap(samples, rate));
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results[0]!.score).toBeGreaterThan(0);
expect(results[0]!.uart).toBeDefined();
});

it("returns multiple results for multi-protocol signal (UART + I2C)", () => {
// UART on channel 0, I2C on channels 1+2
const uartSamples = buildUartSignal([0x41, 0x42, 0x43], 10, 0);
const i2cSamples = buildI2cSignal(0x50, true, [{ byte: 0xAB, ack: true }, { byte: 0xCD, ack: true }], 10, 1, 2);
// Merge: combine byte-by-byte (both signals in same sample stream)
const len = Math.max(uartSamples.length, i2cSamples.length);
const merged = new Uint8Array(len);
for (let i = 0; i < len; i++) {
let byte = 0;
if (i < uartSamples.length) byte |= uartSamples[i]!;
if (i < i2cSamples.length) byte |= i2cSamples[i]!;
merged[i] = byte;
}
const results = autoDetectLaProtocolMulti(cap(merged, 1_000_000));
expect(results.length).toBeGreaterThanOrEqual(2);
const tabs = results.map(r => r.tab);
expect(tabs).toContain("uart");
expect(tabs).toContain("i2c");
});

it("autoDetectLaProtocol returns same top result as autoDetectLaProtocolMulti[0]", () => {
const rate = 1_152_000;
const samples = buildUartSignal([0x48, 0x65, 0x6c], 10, 2);
const single = autoDetectLaProtocol(cap(samples, rate));
const multi = autoDetectLaProtocolMulti(cap(samples, rate));
if (multi.length > 0) {
expect(single.tab).toBe(multi[0]!.tab);
expect(single.score).toBe(multi[0]!.score);
}
});

it("returns results sorted by score descending", () => {
const samples = generateLaTestPattern(1_000_000, 1).samples;
const results = autoDetectLaProtocolMulti(cap(samples, 1_000_000));
for (let i = 1; i < results.length; i++) {
expect(results[i]!.score).toBeLessThanOrEqual(results[i - 1]!.score);
}
});
});

// ---------------------------------------------------------------------------
// decodeCAN
// ---------------------------------------------------------------------------
Expand Down
Loading