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
19 changes: 16 additions & 3 deletions src/dataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { messenger } from './bus.js';
import { projectManager } from './projectmanager.js';
import { dbManager } from './dbmanager.js';
import { signalRegistry } from './signalregistry.js';

/**
* DataProcessor Module
Expand Down Expand Up @@ -42,14 +43,12 @@
console.error('Config Loader:', error);
try {
Config.ANOMALY_TEMPLATES = {};
} catch (e) {

Check warning on line 46 in src/dataprocessor.js

View workflow job for this annotation

GitHub Actions / validate_and_build

'e' is defined but never used

Check warning on line 46 in src/dataprocessor.js

View workflow job for this annotation

GitHub Actions / validate_and_build

'e' is defined but never used
/* ignore */
}
}
}

// --- Local File Handling ---

handleLocalFile(event) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
Expand Down Expand Up @@ -204,12 +203,26 @@
const dictionary = data.signal_dictionary || {};
const series = data.series || {};

// Pre-compute canonical names from the dictionary to avoid lookups in the loop
const mappedDictionary = {};
for (const [id, rawLocalizedName] of Object.entries(dictionary)) {
const nameFromId = signalRegistry.getCanonicalByPid(id);

mappedDictionary[id] =
nameFromId ||
signalRegistry.getCanonicalKey(rawLocalizedName) ||
rawLocalizedName;
}

// Iterate through the series
for (const [signalId, vectors] of Object.entries(series)) {
const signalName = dictionary[signalId] || signalId;
const signalName = mappedDictionary[signalId] || signalId;

const times = vectors.t || [];
const values = vectors.v || [];

const length = Math.min(times.length, values.length);

for (let i = 0; i < length; i++) {
normalized.push({
s: signalName,
Expand Down
2 changes: 2 additions & 0 deletions src/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { xyAnalysis } from './xyanalysis.js';
import { Histogram } from './histogram.js';
import { mathChannels } from './mathchannels.js';
import { projectManager } from './projectmanager.js';
import { signalRegistry } from './signalregistry.js';

window.onload = async function () {
await dataProcessor.loadConfiguration();
Expand All @@ -31,6 +32,7 @@ window.onload = async function () {
xyAnalysis.init();
Histogram.init();
projectManager.init();
signalRegistry.init();

const fileInput = DOM.get('fileInput');
if (fileInput) {
Expand Down
157 changes: 144 additions & 13 deletions src/signalregistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,90 @@ import signalConfig from './signals.json';
class SignalRegistry {
constructor() {
this.mappings = {};
this.metadata = {}; // Stores units, min, max, etc.
this.pidMap = {}; // Maps PIDs directly to canonical keys
this.defaultSignals = [];
this.init();

// Synchronous setup of local defaults and aliases
this._initLocal();
}

init() {
_initLocal() {
signalConfig.forEach((entry) => {
this.mappings[entry.name] = entry.aliases || [];

if (entry.default) {
this.defaultSignals.push(entry.name);
}
// Initialize base metadata for local signals
this.metadata[entry.name] = {
units: '',
min: null,
max: null,
pid: null,
};
});
}

/**
* Fetches metadata from multiple ObdMetrics definitions and merges them.
*/
async init(
urls = [
'https://raw.githubusercontent.com/tzebrowski/ObdMetrics/v11.x/src/main/resources/giulia_2.0_gme.json',
'https://raw.githubusercontent.com/tzebrowski/ObdMetrics/v11.x/src/main/resources/alfa.json',
]
) {
try {
// Ensure we are working with an array
const urlList = Array.isArray(urls) ? urls : [urls];

// Fetch all URLs in parallel
const fetchPromises = urlList.map(async (url) => {
try {
const response = await fetch(url);
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);

const data = await response.json();
this.#mergeMetadata(data);

// Log just the filename for cleaner console output
const fileName = url.substring(url.lastIndexOf('/') + 1);
console.log(`SignalRegistry: Loaded metadata from ${fileName}`);
} catch (err) {
console.error(`SignalRegistry: Failed to load from ${url}`, err);
}
});

await Promise.all(fetchPromises);
console.log(
`SignalRegistry: All remote metadata loaded successfully. Total PIDs mapped: ${Object.keys(this.pidMap).length}`
);
} catch (error) {
console.error(
'SignalRegistry: Critical error fetching remote metadata.',
error
);
}
}

/**
* Retrieve metadata for charting (units, min, max limits).
*/
getSignalMetadata(canonicalKey) {
return this.metadata[canonicalKey] || null;
}

/**
* Retrieve the canonical name based strictly on the PID/ID.
*/
getCanonicalByPid(pid) {
if (!pid) return null;
return this.pidMap[String(pid).toLowerCase()] || null;
}

/**
* Returns the list of canonical signal names that should be shown by default.
* @returns {string[]} Array of canonical keys (e.g., ['Engine Speed', 'Gas Pedal Position'])
*/
getDefaultSignals() {
return this.defaultSignals;
Expand All @@ -32,24 +99,19 @@ class SignalRegistry {
findSignal(canonicalKey, availableSignals) {
if (!availableSignals || availableSignals.length === 0) return null;

// Direct exact match
if (availableSignals.includes(canonicalKey)) return canonicalKey;

const aliases = this.mappings[canonicalKey] || [];

// Exact alias match (Case-insensitive)
for (const alias of aliases) {
const match = availableSignals.find(
(s) => s.toLowerCase() === alias.toLowerCase()
);
if (match) return match;
}

// Smart Word-Boundary Match (Replaces .includes())
// Uses Regex \b to ensure "lat" matches "GPS Lat" but NOT "Calculated"
for (const alias of aliases) {
try {
// Escape special regex characters to prevent errors if alias has symbols like "+" or "("
const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`\\b${escapedAlias}\\b`, 'i');

Expand All @@ -64,9 +126,7 @@ class SignalRegistry {
}

/**
* Checks if a raw signal name (e.g. "RPM") maps to a default canonical signal.
* @param {string} rawSignalName - The signal name from the file
* @returns {boolean} True if this signal should be shown by default
* Checks if a raw signal name maps to a default canonical signal.
*/
isDefaultSignal(rawSignalName) {
const canonical = this.getCanonicalKey(rawSignalName);
Expand All @@ -80,7 +140,6 @@ class SignalRegistry {
for (const [key, aliases] of Object.entries(this.mappings)) {
if (key === rawSignalName) return key;

// Also updated to use word boundaries for reverse lookup safety
if (
aliases.some((alias) => {
if (rawSignalName.toLowerCase() === alias.toLowerCase()) return true;
Expand All @@ -97,6 +156,78 @@ class SignalRegistry {
}
return rawSignalName;
}

#mergeMetadata(data) {
let loadedPids = 0;
let metricsArray = [];

// ObdMetrics JSON files group PIDs into distinct categories (livedata, metadata, capabilities, etc.)
// We need to iterate over all these groups and flatten them into one big array.
if (Array.isArray(data)) {
metricsArray = data;
} else if (data && typeof data === 'object') {
Object.values(data).forEach((value) => {
if (Array.isArray(value)) {
// Merge every array we find (livedata, metadata, etc.) together
metricsArray = metricsArray.concat(value);
}
});
}

if (metricsArray.length === 0) {
console.error(
'SignalRegistry: Failed to parse remote metrics. No arrays found in JSON:',
data
);
return;
}

metricsArray.forEach((metric) => {
if (!metric || typeof metric !== 'object') return;

const rawDesc = metric.description || '';
if (!rawDesc) return;

const cleanName = rawDesc.split('\n')[0].trim();
const canonicalKey = this.getCanonicalKey(cleanName) || cleanName;

this.metadata[canonicalKey] = {
...this.metadata[canonicalKey],
units: metric.units || '',
min: metric.min !== undefined ? parseFloat(metric.min) : null,
max: metric.max !== undefined ? parseFloat(metric.max) : null,
};

let mappedSomething = false;

// ObdMetrics definitions use both an internal "id" (e.g., "7040") and an OBD "pid" (e.g., "1001").
// We map BOTH so that the log file can match against either identifier perfectly.
[metric.id, metric.pid, metric.command].forEach((identifier) => {
if (identifier) {
const cleanId = String(identifier)
.replace(/^(pid_|0x)/i, '')
.toLowerCase();
this.pidMap[cleanId] = canonicalKey;
mappedSomething = true;
}
});

if (mappedSomething) loadedPids++;

if (!this.mappings[canonicalKey]) {
this.mappings[canonicalKey] = [cleanName];
}

const rawAlias = rawDesc.replace(/\n/g, ' ').trim();
if (!this.mappings[canonicalKey].includes(rawAlias)) {
this.mappings[canonicalKey].push(rawAlias);
}
});

console.log(
`SignalRegistry: Successfully mapped ${loadedPids} remote metrics to canonical names.`
);
}
}

export const signalRegistry = new SignalRegistry();
5 changes: 2 additions & 3 deletions tests/dataprocessor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -868,11 +868,10 @@ describe('DataProcessor: Columnar JSON Support', () => {
expect(file.metadata['trip.duration']).toBe('3600');

// Available signals check (should map IDs to human-readable names)
expect(file.availableSignals).toContain('Boost Pressure');
expect(file.availableSignals).toContain('Engine RPM');
expect(file.availableSignals).toContain('Boost');

// Series data check (un-pivoted successfully)
const boostData = file.signals['Boost Pressure'];
const boostData = file.signals['Boost'];
expect(boostData).toHaveLength(2);
expect(boostData[0].x).toBe(1000); // timestamp
expect(boostData[0].y).toBe(14.1); // value
Expand Down
Loading