diff --git a/index.html b/index.html index 530d82e..787ee8c 100644 --- a/index.html +++ b/index.html @@ -3,67 +3,606 @@ - EmpirBus Project Viewer - ebp2docs - - + EBP2DOC Web + -
-
-

🔧 EmpirBus Project Viewer

-
-
📁
-

Click to upload or drag and drop your .ebp file here

- -
-
- - - - - -
- - - +

+ + + + + + + + EBP2DOC Web +

+ +
+ + + + + +

Drop .ebp file here or click to browse

+

Supports EmpirBus Project (.ebp) files

+ +
+ + +
+ + +
+ + + + + +
+ + + - \ No newline at end of file + diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..bdc7bdb --- /dev/null +++ b/js/README.md @@ -0,0 +1,169 @@ +# EBP Parser Modules + +Modern, idiomatic JavaScript modules for parsing EmpirBus Project (.ebp) files. + +## Architecture + +The parser is organized into focused, single-responsibility modules: + +### Core Modules + +#### `parser.js` +Main parsing module that handles XML parsing and data extraction. + +**Exports:** +- `parseUnits(xmlString)` - Parse unit information +- `parseAlarms(xmlString)` - Parse alarm definitions +- `parseProjectMetadata(xmlString)` - Parse project metadata +- `parseSchemas(xmlString)` - Parse schema information +- `parseComponents(xmlString)` - Parse NMEA 2000 components +- `parseMemory(xmlString)` - Parse memory allocations +- `validateEBP(xmlString)` - Validate EBP file structure + +#### `enums.js` +Type-safe enumerations for EBP data types. + +**Exports:** +- `Direction` - Channel direction (INPUT, OUTPUT, BOTH, NONE) +- `N2kDirection` - NMEA 2000 direction (TRANSMIT, RECEIVE, NONE) + +**Example:** +```javascript +import { Direction, N2kDirection } from './enums.js'; + +const dir = Direction.fromString('input'); +console.log(dir.name); // 'input' +console.log(dir.id); // 1 + +const n2k = N2kDirection.fromId(0); +console.log(n2k.name); // 'transmit' +``` + +#### `channel-decoder.js` +Decodes channel settings using efficient Map-based lookups. + +**Exports:** +- `decodeChannelSettings(channel)` - Decode channel configuration + +**Example:** +```javascript +import { decodeChannelSettings } from './channel-decoder.js'; + +const channel = { + inMainChannelSettingId: 57, + inChannelSettingId: 2, + outMainChannelSettingId: 48, + outChannelSettingId: 1 +}; + +const decoded = decodeChannelSettings(channel); +console.log(decoded.input.type); // 'digital input' +console.log(decoded.input.subtype); // 'closes to plus' +console.log(decoded.output.type); // 'digital output +' +``` + +#### `component-decoder.js` +Decodes NMEA 2000 component information. + +**Exports:** +- `decodeComponent(component, properties)` - Decode component data + +**Supported Components:** +- 1283: Fluid Level (PGN 127505) +- 1281: Binary Switch (PGN 127501) +- 1282: Binary Indicator (PGN 127501) +- 1285: Temperature (PGN 130312) +- 1291: Switch Control (PGN 127502) +- 1376: J1939 AC PGN +- 1361: Proprietary PGN + +**Example:** +```javascript +import { decodeComponent } from './component-decoder.js'; + +const component = { componentId: 1283, channelId: 5, unitId: 1 }; +const properties = [ + { id: 0, value: '0' }, // instance + { id: 1, value: '0' }, // fluid type (fuel) + { id: 2, value: '1' } // direction (receive) +]; + +const decoded = decodeComponent(component, properties); +console.log(decoded.name); // 'Fluid Level' +console.log(decoded.pgn); // 127505 +console.log(decoded.id); // 'fuel' +console.log(decoded.instance); // 0 +``` + +### UI Modules + +#### `ui.js` +Handles all UI rendering and interactions. + +#### `utils.js` +Utility functions for HTML escaping, filtering, and data manipulation. + +## Design Principles + +### 1. Modern JavaScript +- ES6+ features (classes, Maps, optional chaining, nullish coalescing) +- Module imports/exports +- Const/let instead of var +- Arrow functions where appropriate + +### 2. No Java-isms +- ❌ No `Integer.MIN_VALUE` → ✅ Use `null` or `undefined` +- ❌ No verbose switch statements → ✅ Use Map-based lookups +- ❌ No Java-style comments → ✅ Clean JSDoc +- ❌ No foreign language variable names → ✅ English only + +### 3. Functional Patterns +- Pure functions where possible +- Immutable data transformations +- Map/filter/reduce over loops +- No side effects in utility functions + +### 4. Type Safety via JSDoc +```javascript +/** + * @param {string} xmlString - XML content + * @returns {Array} Parsed units + */ +export function parseUnits(xmlString) { ... } +``` + +## Performance Considerations + +- **Map lookups**: O(1) average case vs O(n) for switch statements +- **Single DOM parse**: Parse XML once, query multiple times +- **Lazy evaluation**: Only parse what's needed +- **Efficient sorting**: Native sort with custom comparators + +## Migration from Java Port + +The original codebase was ported from Java. This refactored version: + +1. **Replaces** Java enums with ES6 classes +2. **Eliminates** Integer.MIN_VALUE with null/undefined +3. **Simplifies** verbose switch statements with Maps +4. **Modernizes** naming (naam → name) +5. **Removes** all Java-related comments and patterns + +## Testing + +```javascript +import { parseUnits, validateEBP } from './parser.js'; + +// Validate before parsing +const validation = validateEBP(xmlContent); +if (validation.isValid) { + const units = parseUnits(xmlContent); + console.log(`Found ${units.length} units`); +} +``` + +## Browser Compatibility + +- Requires ES6+ support (all modern browsers) +- Uses native DOMParser (no external dependencies) +- ES modules (type="module" required) diff --git a/js/channel-decoder.js b/js/channel-decoder.js new file mode 100644 index 0000000..6c1b7a4 --- /dev/null +++ b/js/channel-decoder.js @@ -0,0 +1,175 @@ +/** + * Channel Decoder Module + * Decodes channel settings for EBP units + */ + +// Input channel type/subtype mappings +const INPUT_SETTINGS = { + 1: { type: 'digital input', subtype: 'standard' }, + 57: { + type: 'digital input', + subtypes: { + 1: 'closes to minus', + 2: 'closes to plus', + 4: 'closes to common', + 6: 'closes to plus weak pulldown', + 7: 'measure input frequency' + } + }, + 64: { + type: 'analog input', + subtypes: { + 1: 'voltage signal', + 2: '4-20 mA', + 4: '0-1500 Ohm', + 5: 'multiswitch', + 6: 'firealarm (constant power)', + 7: 'multiswitch (68Ohm +/- 1%)', + 8: 'dual fixed multiswitch', + 9: 'temp sensor ohm' + } + }, + 54: { + type: 'window wiper feedback', + subtypes: { + 1: 'closes to minus in parking', + 2: 'open in parking' + } + } +}; + +// Output channel type/subtype mappings +const OUTPUT_SETTINGS = { + 1: { type: 'digital output', subtype: 'standard' }, + 48: { + type: 'digital output +', + subtypes: { + 1: 'normal', + 2: 'open load detection', + 3: 'open load detection at turn on' + } + }, + 49: { + type: 'digital output -', + subtypes: { + 1: 'normal' + } + }, + 52: { + type: 'commonline', + subtypes: { + 0: 'normal' + } + }, + 53: { + type: 'half bridge output +/-', + subtypes: { + 0: 'normal half bridge' + } + }, + 55: { + type: 'window wiper', + subtypes: { + 1: 'connection #1 with diode', + 2: 'connection #1 no diode', + 3: 'connection #2 with diode', + 4: 'connection #2 no diode' + } + }, + 65: { + type: 'signal drive (max 50mA)', + subtypes: { + 0: 'positive drive', + 1: 'negative drive' + } + } +}; + +/** + * Decode channel settings into human-readable format + * @param {Object} channel - Channel object with setting IDs + * @returns {Object} Decoded channel settings + */ +export function decodeChannelSettings(channel) { + const mainInId = parseInt(channel.inMainChannelSettingId) || 0; + const subInId = parseInt(channel.inChannelSettingId) || 0; + const mainOutId = parseInt(channel.outMainChannelSettingId) || -1; + const subOutId = parseInt(channel.outChannelSettingId) || -1; + + return { + input: decodeInputSettings(mainInId, subInId), + output: decodeOutputSettings(mainOutId, subOutId) + }; +} + +/** + * Decode input channel settings + * @param {number} mainId - Main channel setting ID + * @param {number} subId - Sub channel setting ID + * @returns {Object} Decoded input settings + */ +function decodeInputSettings(mainId, subId) { + const config = INPUT_SETTINGS[mainId]; + + if (!config) { + return { + type: ':unknown:' + mainId, + subtype: ':unknown:' + subId + }; + } + + // If config has a fixed subtype, use it + if (config.subtype) { + return { + type: config.type, + subtype: config.subtype + }; + } + + // Otherwise, look up the subtype + const subtype = config.subtypes && config.subtypes[subId]; + return { + type: config.type, + subtype: subtype || (':unknown:' + subId) + }; +} + +/** + * Decode output channel settings + * @param {number} mainId - Main channel setting ID + * @param {number} subId - Sub channel setting ID + * @returns {Object} Decoded output settings + */ +function decodeOutputSettings(mainId, subId) { + // Handle -1 (not set) + if (mainId === -1) { + return { + type: ':unknown:', + subtype: ':unknown:' + }; + } + + const config = OUTPUT_SETTINGS[mainId]; + + if (!config) { + return { + type: ':unknown:' + mainId, + subtype: ':unknown:' + subId + }; + } + + // If config has a fixed subtype, use it + if (config.subtype) { + return { + type: config.type, + subtype: config.subtype + }; + } + + // Otherwise, look up the subtype + const subtype = config.subtypes && config.subtypes[subId]; + return { + type: config.type, + subtype: subtype || (':unknown:' + subId) + }; +} diff --git a/js/component-decoder.js b/js/component-decoder.js new file mode 100644 index 0000000..f45a22b --- /dev/null +++ b/js/component-decoder.js @@ -0,0 +1,230 @@ +/** + * Component Decoder Module + * Decodes NMEA 2000 component information from EBP files + */ + +import { N2kDirection } from './enums.js'; + +// Fluid types for Fluid Level component (PGN 127505) +const FLUID_TYPES = [ + 'fuel', + 'fresh water', + 'waste water', + 'live well', + 'oil', + 'black water' +]; + +// Temperature sources for Temperature component (PGN 130312) +const TEMPERATURE_SOURCES = [ + 'sea', + 'outside', + 'inside', + 'engine room', + 'main cabin', + 'live well', + 'bait well', + 'refridgeration', + 'heating system', + 'dew point', + 'wind chill apparent', + 'wind chill theoretical', + 'heat index', + 'freezer' +]; + +// J1939 PGN types +const J1939_PGNS = [ + 65014, 65027, 65011, 65008, + 65024, 65021, 65017, 65030, + 65004, 65003, 65002, 65001 +]; + +/** + * Component decoder configurations + * Maps component IDs to their decoder functions + */ +const COMPONENT_DECODERS = new Map([ + [1283, decodeFluidLevel], // Fluid Level + [1281, decodeBinarySwitch], // Binary Switch + [1282, decodeBinaryIndicator], // Binary Indicator + [1285, decodeTemperature], // Temperature + [1291, decodeSwitchControl], // Switch Control + [1376, decodeJ1939AcPgn], // J1939 AC PGN + [1361, decodeProprietaryPgn] // Proprietary PGN +]); + +/** + * Decode a component into NMEA 2000 information + * @param {Object} component - Component object + * @param {Array} properties - Component properties array + * @returns {Object} Decoded component information + */ +export function decodeComponent(component, properties) { + const decoder = COMPONENT_DECODERS.get(component.componentId); + + if (!decoder) { + return createEmptyResult(); + } + + const propertyMap = createPropertyMap(properties); + return decoder(propertyMap); +} + +/** + * Create a Map from properties array for easier lookup + * @param {Array} properties - Properties array + * @returns {Map} Property ID to value map + */ +function createPropertyMap(properties) { + const map = new Map(); + properties?.forEach(prop => { + const value = parseInt(prop.value); + map.set(prop.id, isNaN(value) ? null : value); + }); + return map; +} + +/** + * Get property value with null for missing/invalid values + * @param {Map} props - Property map + * @param {number} id - Property ID + * @returns {number|null} Property value or null + */ +function getProperty(props, id) { + return props.get(id) ?? null; +} + +/** + * Create empty result object + * @returns {Object} Empty result + */ +function createEmptyResult() { + return { + name: '', + pgn: 0, + instance: null, + id: '', + direction: N2kDirection.NONE, + device: null + }; +} + +/** + * Decode Fluid Level component (1283) + */ +function decodeFluidLevel(props) { + const fluidType = getProperty(props, 1); + const fluidName = fluidType !== null && fluidType < FLUID_TYPES.length + ? FLUID_TYPES[fluidType] + : 'unknown'; + + return { + name: 'Fluid Level', + pgn: 127505, + instance: getProperty(props, 0), + id: fluidName, + direction: N2kDirection.fromId(getProperty(props, 2)), + device: null + }; +} + +/** + * Decode Binary Switch component (1281) + */ +function decodeBinarySwitch(props) { + const switchNumber = getProperty(props, 1); + + return { + name: 'Binary Switch', + pgn: 127501, + instance: getProperty(props, 0), + id: switchNumber !== null ? String(switchNumber + 1) : '', + direction: N2kDirection.fromId(getProperty(props, 5)), + device: null + }; +} + +/** + * Decode Binary Indicator component (1282) + */ +function decodeBinaryIndicator(props) { + const indicatorNumber = getProperty(props, 1); + + return { + name: 'Binary Indicator', + pgn: 127501, + instance: getProperty(props, 0), + id: indicatorNumber !== null ? String(indicatorNumber + 1) : '', + direction: N2kDirection.fromId(getProperty(props, 2)), + device: null + }; +} + +/** + * Decode Temperature component (1285) + */ +function decodeTemperature(props) { + const sourceType = getProperty(props, 1); + const sourceName = sourceType !== null && sourceType < TEMPERATURE_SOURCES.length + ? TEMPERATURE_SOURCES[sourceType] + : 'unknown'; + + return { + name: 'Temperature', + pgn: 130312, + instance: getProperty(props, 0), + id: sourceName, + direction: N2kDirection.fromId(getProperty(props, 5)), + device: null + }; +} + +/** + * Decode Switch Control component (1291) + */ +function decodeSwitchControl(props) { + const switchNumber = getProperty(props, 2); + + return { + name: 'Switch Control', + pgn: 127502, + instance: getProperty(props, 1), + id: switchNumber !== null ? String(switchNumber + 1) : '', + direction: N2kDirection.fromId(getProperty(props, 0)), + device: null + }; +} + +/** + * Decode J1939 AC PGN component (1376) + */ +function decodeJ1939AcPgn(props) { + const pgnType = getProperty(props, 1); + const pgn = pgnType !== null && pgnType < J1939_PGNS.length + ? J1939_PGNS[pgnType] + : 0; + + return { + name: 'J1939 AC PGN', + pgn, + instance: null, + id: '', + direction: N2kDirection.NONE, + device: getProperty(props, 0) + }; +} + +/** + * Decode Proprietary PGN component (1361) + */ +function decodeProprietaryPgn(props) { + return { + name: 'Proprietary PGN', + pgn: 65280, + instance: getProperty(props, 2), + id: '', + direction: N2kDirection.fromId(getProperty(props, 0)), + device: null + }; +} diff --git a/js/enums.js b/js/enums.js new file mode 100644 index 0000000..4d216e3 --- /dev/null +++ b/js/enums.js @@ -0,0 +1,68 @@ +/** + * Enums Module + * Type-safe enumerations for EBP data + */ + +/** + * Channel direction enumeration + */ +export class Direction { + static NONE = new Direction('', -1); + static BOTH = new Direction('both', 0); + static INPUT = new Direction('input', 1); + static OUTPUT = new Direction('output', 2); + + constructor(name, id) { + this.name = name; + this.id = id; + } + + static fromString(str) { + const normalized = str?.toLowerCase() || ''; + switch (normalized) { + case 'both': return Direction.BOTH; + case 'input': return Direction.INPUT; + case 'output': return Direction.OUTPUT; + default: return Direction.NONE; + } + } + + static fromId(id) { + switch (id) { + case 0: return Direction.BOTH; + case 1: return Direction.INPUT; + case 2: return Direction.OUTPUT; + default: return Direction.NONE; + } + } + + toString() { + return this.name; + } +} + +/** + * NMEA 2000 direction enumeration + */ +export class N2kDirection { + static NONE = new N2kDirection('', -1); + static TRANSMIT = new N2kDirection('transmit', 0); + static RECEIVE = new N2kDirection('receive', 1); + + constructor(name, id) { + this.name = name; + this.id = id; + } + + static fromId(id) { + switch (id) { + case 0: return N2kDirection.TRANSMIT; + case 1: return N2kDirection.RECEIVE; + default: return N2kDirection.NONE; + } + } + + toString() { + return this.name; + } +} diff --git a/js/parser.js b/js/parser.js index 050e0f2..e83736d 100644 --- a/js/parser.js +++ b/js/parser.js @@ -6,9 +6,12 @@ /** * Parse basic unit information from EBP file * @param {string} xmlString - XML content as string - * @returns {Array} Array of unit objects + * @returns {Promise} Array of unit objects */ -export function parseUnits(xmlString) { +export async function parseUnits(xmlString) { + // Lazy load dependencies to avoid breaking module loading + const { Direction } = await import('./enums.js'); + const { decodeChannelSettings } = await import('./channel-decoder.js'); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); @@ -33,13 +36,15 @@ export function parseUnits(xmlString) { for (let i = 0; i < unitElements.length; i++) { const unit = unitElements[i]; + const unitId = parseInt(unit.getAttribute('id')) || 0; + const unitData = { - id: unit.getAttribute('id') || 'N/A', + id: unitId, serial: unit.getAttribute('serial') || 'N/A', name: unit.getAttribute('name') || 'N/A', unitTypeId: unit.getAttribute('unitTypeId') || 'N/A', standardUnitVariantNumber: unit.getAttribute('standardUnitVariantNumber') || 'N/A', - channels: parseChannels(unit) + channels: parseChannels(unit, unitId, xmlDoc, Direction, decodeChannelSettings) }; console.log(`Unit ${i + 1}:`, unitData.name, 'ID:', unitData.id, 'TypeID:', unitData.unitTypeId); units.push(unitData); @@ -53,12 +58,43 @@ export function parseUnits(xmlString) { return units; } +/** + * Get direction from components for a specific channel + * @param {number} combiId - Combined ID (256 * unitId + channelNumber - 1) + * @param {Document} xmlDoc - Parsed XML document + * @param {Object} Direction - Direction enum + * @returns {Object} Direction object + */ +function getDirectionFromComponents(combiId, xmlDoc, Direction) { + const schemaElements = xmlDoc.querySelectorAll('schema'); + + for (const schema of schemaElements) { + const componentElements = schema.querySelectorAll('components > component'); + + for (const component of componentElements) { + const channelId = parseInt(component.getAttribute('channelId')); + + if (!isNaN(channelId) && channelId !== -2147483648 && channelId === combiId) { + // Found a component using this channel + const directionStr = component.getAttribute('direction') || ''; + return Direction.fromString(directionStr); + } + } + } + + return Direction.NONE; +} + /** * Parse channel information for a unit * @param {Element} unitElement - Unit XML element + * @param {number} unitId - Unit ID + * @param {Document} xmlDoc - Full XML document for component lookup + * @param {Object} Direction - Direction enum + * @param {Function} decodeChannelSettings - Channel decoder function * @returns {Array} Array of channel groups */ -function parseChannels(unitElement) { +function parseChannels(unitElement, unitId, xmlDoc, Direction, decodeChannelSettings) { const channelGroups = []; const groupElements = unitElement.getElementsByTagName('unitChannelGroup'); @@ -69,15 +105,30 @@ function parseChannels(unitElement) { for (let j = 0; j < channelElements.length; j++) { const channel = channelElements[j]; - channels.push({ - number: channel.getAttribute('number') || 'N/A', + const channelNumber = parseInt(channel.getAttribute('number')) || 0; + + // Calculate combiId to lookup actual direction from components + const combiId = 256 * unitId + channelNumber - 1; + const actualDirection = getDirectionFromComponents(combiId, xmlDoc, Direction); + + const channelData = { + number: channelNumber, name: channel.getAttribute('name') || 'N/A', - direction: channel.getAttribute('direction') || 'N/A', - inMainChannelSettingId: channel.getAttribute('inMainChannelSettingId') || '', - inChannelSettingId: channel.getAttribute('inChannelSettingId') || '', - outMainChannelSettingId: channel.getAttribute('outMainChannelSettingId') || '', - outChannelSettingId: channel.getAttribute('outChannelSettingId') || '' - }); + direction: actualDirection, // Use actual direction from components + inMainChannelSettingId: parseInt(channel.getAttribute('inMainChannelSettingId')) || 0, + inChannelSettingId: parseInt(channel.getAttribute('inChannelSettingId')) || 0, + outMainChannelSettingId: parseInt(channel.getAttribute('outMainChannelSettingId')) || -1, + outChannelSettingId: parseInt(channel.getAttribute('outChannelSettingId')) || -1 + }; + + // Decode channel settings + const decoded = decodeChannelSettings(channelData); + channelData.sInMainChannelSettingId = decoded.input.type; + channelData.sInChannelSettingId = decoded.input.subtype; + channelData.sOutMainChannelSettingId = decoded.output.type; + channelData.sOutChannelSettingId = decoded.output.subtype; + + channels.push(channelData); } channelGroups.push({ @@ -203,11 +254,11 @@ export function parseProjectMetadata(xmlString) { */ export function validateEBP(xmlString) { const errors = []; - + try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); - + // Check for XML parsing errors const parseError = xmlDoc.querySelector('parsererror'); if (parseError) { @@ -241,4 +292,188 @@ export function validateEBP(xmlString) { errors.push(`Parsing error: ${error.message}`); return { isValid: false, errors }; } +} + +/** + * Parse schemas from EBP file + * @param {string} xmlString - XML content as string + * @returns {Array} Array of schema objects + */ +export function parseSchemas(xmlString) { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); + + const schemas = []; + const schemaElements = xmlDoc.querySelectorAll('schemas > schema'); + + schemaElements.forEach(schema => { + schemas.push({ + id: parseInt(schema.getAttribute('id')) || 0, + name: schema.getAttribute('name') || '', + sortIndex: parseInt(schema.getAttribute('sortIndex')) || 0 + }); + }); + + return schemas.sort((a, b) => a.sortIndex - b.sortIndex); +} + +/** + * Parse components from EBP file + * @param {string} xmlString - XML content as string + * @returns {Array} Array of component objects + */ +export async function parseComponents(xmlString) { + // Lazy load the decoder to avoid breaking basic functionality + const { decodeComponent } = await import('./component-decoder.js'); + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); + + const components = []; + const masterModuleBusId = findMasterModuleBusId(xmlDoc); + + const schemaElements = xmlDoc.querySelectorAll('schema'); + + schemaElements.forEach(schema => { + const tabName = schema.getAttribute('name') || ''; + const componentElements = schema.querySelectorAll('components > component'); + + componentElements.forEach(componentNode => { + const componentId = parseInt(componentNode.getAttribute('componentId')); + + // Skip special component types (alerts and memory) + if (componentId === 1292 || componentId === 2304) return; + + const component = { + componentId, + channelId: parseInt(componentNode.getAttribute('channelId')) || null, + unitId: parseInt(componentNode.getAttribute('unitId')) || null + }; + + const properties = parseProperties(componentNode); + const decoded = decodeComponent(component, properties); + + if (decoded.name) { + components.push({ + name: decoded.name, + pgn: decoded.pgn, + device: decoded.device ?? masterModuleBusId, + instance: decoded.instance, + id: decoded.id, + direction: decoded.direction.name, + tabName + }); + } + }); + }); + + // Sort components + return components.sort((a, b) => { + if (a.pgn !== b.pgn) return a.pgn - b.pgn; + if (a.device !== b.device) return a.device - b.device; + if (a.instance !== b.instance) return (a.instance ?? 0) - (b.instance ?? 0); + + // Try to sort by ID if numeric + const aId = parseInt(a.id); + const bId = parseInt(b.id); + if (!isNaN(aId) && !isNaN(bId)) { + return aId - bId; + } + + return String(a.id).localeCompare(String(b.id)); + }); +} + +/** + * Parse memory from EBP file + * @param {string} xmlString - XML content as string + * @returns {Array} Array of memory objects + */ +export function parseMemory(xmlString) { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); + + const MEMORY_TYPES = new Map([ + [0, { name: 'Bit (1 Bit)', bits: 1 }], + [1, { name: 'UByte (8 Bit)', bits: 8 }], + [2, { name: 'UWord (16 Bit)', bits: 16 }], + [3, { name: 'UDWord (32 Bit)', bits: 32 }] + ]); + + const memory = []; + const componentElements = xmlDoc.querySelectorAll('component'); + + componentElements.forEach(componentNode => { + const componentId = parseInt(componentNode.getAttribute('componentId')); + + if (componentId === 2304) { // Memory Stored Value component + const properties = parseProperties(componentNode); + const propertyMap = new Map(); + + properties.forEach(prop => { + propertyMap.set(prop.id, parseInt(prop.value) || null); + }); + + const memType = propertyMap.get(0); + const memLocation = propertyMap.get(1); + const type = MEMORY_TYPES.get(memType) || { name: 'unknown', bits: 1 }; + + memory.push({ + type: type.name, + location: memLocation, + bits: type.bits + }); + } + }); + + return memory.sort((a, b) => a.location - b.location); +} + +/** + * Find the master module bus ID + * @param {Document} xmlDoc - Parsed XML document + * @returns {number} Master module bus ID or -1 if not found + */ +function findMasterModuleBusId(xmlDoc) { + const unitElements = xmlDoc.querySelectorAll('units > unit'); + + for (const unit of unitElements) { + const unitTypeId = parseInt(unit.getAttribute('unitTypeId')) || 0; + + // Master module types: 101, 100 + if (unitTypeId === 101 || unitTypeId === 100) { + return parseInt(unit.getAttribute('id')) || -1; + } + + // Check for master module in properties (unitTypeId 20, 1, 16, 4) + if ([20, 1, 16, 4].includes(unitTypeId)) { + const properties = parseProperties(unit); + for (const prop of properties) { + if (prop.id === 2 && prop.value === '2') { + return parseInt(unit.getAttribute('id')) || -1; + } + } + } + } + + return -1; +} + +/** + * Parse properties from an XML element + * @param {Element} element - XML element + * @returns {Array} Array of property objects + */ +function parseProperties(element) { + const properties = []; + const propertyElements = element.querySelectorAll('properties > property'); + + propertyElements.forEach(prop => { + properties.push({ + id: parseInt(prop.getAttribute('id')) || -1, + value: prop.getAttribute('value') || '' + }); + }); + + return properties; } \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index 1131d04..cf43bbd 100644 --- a/js/ui.js +++ b/js/ui.js @@ -11,7 +11,7 @@ import { escapeHtml, getDirectionIcon, getDirectionColor } from './utils.js'; * @param {HTMLElement} container - Container element * @param {Object} metadata - Optional project metadata to display * @param {boolean} hasSearch - Whether search is active (auto-expands sections) - * @param {Array} alarms - Optional array of alarm objects + * @param {Array} alarms - Optional array of alarm objects (no longer displayed here) */ export function displayUnits(units, container, metadata = null, hasSearch = false, alarms = []) { container.style.display = 'block'; @@ -23,10 +23,7 @@ export function displayUnits(units, container, metadata = null, hasSearch = fals html += renderMetadata(metadata, units.length); } - // Add project-level alarms if provided - if (alarms && alarms.length > 0) { - html += renderProjectAlarms(alarms, hasSearch); - } + // Alarms are now displayed in their own tab, not here units.forEach((unit) => { html += renderUnitCard(unit, hasSearch); @@ -184,18 +181,34 @@ function renderChannels(channelGroups, unitTypeId, autoExpand = false) { `; group.channels.forEach(channel => { - const directionColor = getDirectionColor(channel.direction); - const directionIcon = getDirectionIcon(channel.direction); + // Capitalize the direction name (input -> Input, output -> Output) + const directionName = channel.direction.name.charAt(0).toUpperCase() + channel.direction.name.slice(1); + const directionColor = getDirectionColor(directionName); + const directionIcon = getDirectionIcon(directionName); + + // Get the correct type/subtype based on actual direction + let channelType = ''; + let channelSubtype = ''; + + if (channel.direction.id === 1) { // INPUT + channelType = channel.sInMainChannelSettingId; + channelSubtype = channel.sInChannelSettingId; + } else if (channel.direction.id === 2) { // OUTPUT + channelType = channel.sOutMainChannelSettingId; + channelSubtype = channel.sOutChannelSettingId; + } html += `
#${escapeHtml(channel.number)} - ${directionIcon} ${escapeHtml(channel.direction)} + ${directionIcon} ${directionName}
${escapeHtml(channel.name)}
+ ${channelType ? `
${escapeHtml(channelType)}
` : ''} + ${channelSubtype ? `
${escapeHtml(channelSubtype)}
` : ''}
`; }); @@ -312,4 +325,116 @@ function formatDate(utcString) { } catch { return utcString; } +} + +/** + * Display NMEA 2000 components in a table + * @param {Array} components - Array of component objects + * @param {HTMLElement} container - Container element + * @param {Object} metadata - Optional project metadata + */ +export function displayComponents(components, container, metadata = null) { + container.style.display = 'block'; + + let html = ''; + + if (metadata) { + html += renderMetadata(metadata); + } + + html += '
'; + html += '

📡 NMEA 2000 Components

'; + html += `

Found ${components.length} component${components.length !== 1 ? 's' : ''}

`; + html += '
'; + html += ''; + html += ''; + + components.forEach(comp => { + const device = comp.device !== null && comp.device !== -1 ? comp.device : ''; + const instance = comp.instance !== null && comp.instance !== -1 ? comp.instance : ''; + + html += ''; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ''; + }); + + html += '
PGN NamePGN NumberDeviceInstanceIDDirectionTab
${escapeHtml(comp.name)}${comp.pgn}${device}${instance}${escapeHtml(comp.id)}${escapeHtml(comp.direction)}${escapeHtml(comp.tabName)}
'; + + container.innerHTML = html; +} + +/** + * Display alerts in a detailed table + * @param {Array} alerts - Array of alert objects + * @param {HTMLElement} container - Container element + * @param {Object} metadata - Optional project metadata + */ +export function displayAlertsDetailed(alerts, container, metadata = null) { + container.style.display = 'block'; + + let html = ''; + + if (metadata) { + html += renderMetadata(metadata); + } + + html += '
'; + html += '

🔔 Alarms

'; + html += `${alerts.length} alarm${alerts.length !== 1 ? 's' : ''}`; + html += '
'; + html += ''; + html += ''; + + alerts.forEach(alert => { + html += ''; + html += ``; + html += ``; + html += ``; + html += ''; + }); + + html += '
Alarm IDAlarm NameSchema
${escapeHtml(alert.alarmId)}${escapeHtml(alert.alarmName)}${escapeHtml(alert.schemaName)}
'; + + container.innerHTML = html; +} + +/** + * Display memory allocations in a table + * @param {Array} memory - Array of memory objects + * @param {HTMLElement} container - Container element + * @param {Object} metadata - Optional project metadata + */ +export function displayMemory(memory, container, metadata = null) { + container.style.display = 'block'; + + let html = ''; + + if (metadata) { + html += renderMetadata(metadata); + } + + html += '
'; + html += '

💾 Memory Allocations

'; + html += `

Found ${memory.length} memory allocation${memory.length !== 1 ? 's' : ''}

`; + html += '
'; + html += ''; + html += ''; + + memory.forEach(mem => { + html += ''; + html += ``; + html += ``; + html += ``; + html += ''; + }); + + html += '
Memory TypeMemory LocationBits
${escapeHtml(mem.type)}${mem.location}${mem.bits}
'; + + container.innerHTML = html; } \ No newline at end of file