/** * Spec Reader - Parses QuickBASIC binary DAT spec files * * Reads model specification data from 4 product family DAT files: * 5BMAIN.DAT (SCM5B family, 160 bytes/record) * 8BMAIN.DAT (8B family, 163 bytes/record) * DSCOUT.DAT (DSCA family, 163 bytes/record) * SCTMAIN.DAT (DSCT family, 121 bytes/record) * * These are QuickBASIC random-access files using TYPE (struct) records. * All values are little-endian: SINGLE = IEEE 754 float (4 bytes), * INTEGER = signed 16-bit (2 bytes), STRING * N = fixed-width ASCII. */ const fs = require('fs'); const path = require('path'); // Default spec data directory const DEFAULT_SPEC_DIR = path.join(__dirname, '..', 'specdata'); // -------------------------------------------------------------------------- // Binary read helpers // -------------------------------------------------------------------------- function readString(buf, offset, length) { return buf.toString('ascii', offset, offset + length).replace(/\0/g, '').trim(); } function readSingle(buf, offset) { return buf.readFloatLE(offset); } function readInteger(buf, offset) { return buf.readInt16LE(offset); } // -------------------------------------------------------------------------- // TYPE definitions (field name, type, size) // -------------------------------------------------------------------------- const FIELD_TYPES = { STRING17: { size: 17, read: (buf, off) => readString(buf, off, 17) }, STRING9: { size: 9, read: (buf, off) => readString(buf, off, 9) }, STRING15: { size: 15, read: (buf, off) => readString(buf, off, 15) }, STRING14: { size: 14, read: (buf, off) => readString(buf, off, 14) }, STRING13: { size: 13, read: (buf, off) => readString(buf, off, 13) }, STRING7: { size: 7, read: (buf, off) => readString(buf, off, 7) }, SINGLE: { size: 4, read: (buf, off) => readSingle(buf, off) }, INTEGER: { size: 2, read: (buf, off) => readInteger(buf, off) }, }; const S15 = 'STRING15'; const S14 = 'STRING14'; const S13 = 'STRING13'; const S7 = 'STRING7'; const SNG = 'SINGLE'; const INT = 'INTEGER'; // SCM5B: 160 bytes/record const SCM5B_FIELDS = [ ['MODNAME', S15], ['SENTYPE', S7], ['ISMAXNEXCL', SNG], ['ISMAXFEXCL', SNG], ['OUTRES', SNG], ['MININ', SNG], ['MAXIN', SNG], ['IEXC', SNG], ['RCONV', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['CALTOL', SNG], ['VEXC', SNG], ['VEXCACC', SNG], ['EXCLOAD', SNG], ['EXCLOADREG', SNG], ['EXCIMAX', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', SNG], ['ATTEN', SNG], ['ATTENTOL', SNG], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['PSS', SNG], ['OUTNOISE', SNG], ['INPUTRES', SNG], ['VOPENINMIN', SNG], ['VOPENINMAX', SNG], ['LEADRERR', SNG], ['LINEARIZED', INT], ['OSCALIN', SNG], ['GNCALIN', SNG], ['BANDWIDTH', SNG], ['IMATCHTOL', SNG], ]; // 8B: 163 bytes/record (no OUTRES, has OUTSIGTYPE) const B8_FIELDS = [ ['MODNAME', S15], ['SENTYPE', S7], ['ISMAXNEXCL', SNG], ['ISMAXFEXCL', SNG], ['MININ', SNG], ['MAXIN', SNG], ['IEXC', SNG], ['RCONV', SNG], ['OUTSIGTYPE', S7], ['MINOUT', SNG], ['MAXOUT', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['CALTOL', SNG], ['VEXC', SNG], ['VEXCACC', SNG], ['EXCLOAD', SNG], ['EXCLOADREG', SNG], ['EXCIMAX', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', SNG], ['ATTEN', SNG], ['ATTENTOL', SNG], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['PSS', SNG], ['OUTNOISE', SNG], ['INPUTRES', SNG], ['VOPENINMIN', SNG], ['VOPENINMAX', SNG], ['LEADRERR', SNG], ['LINEARIZED', INT], ['OSCALIN', SNG], ['GNCALIN', SNG], ['BANDWIDTH', SNG], ['IMATCHTOL', SNG], ]; // DSCA: 163 bytes/record const DSCA_FIELDS = [ ['MODNAME', S13], ['SENTYPE', S7], ['ISMAXNL', SNG], ['ISMAXFL', SNG], ['MININ', SNG], ['MAXIN', SNG], ['RCONV', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['OUTSIGTYPE', S7], ['OSCALIN', SNG], ['GNCALIN', SNG], ['CALTOL', SNG], ['LOAD1', SNG], ['LINEAR1', SNG], ['ACCURACY1', SNG], ['LOAD2', SNG], ['LINEAR2', SNG], ['ACCURACY2', SNG], ['LOAD3', SNG], ['LINEAR3', SNG], ['ACCURACY3', SNG], ['BANDWIDTH', SNG], ['TESTFREQ', SNG], ['ATTEN', SNG], ['ATTENTOL', SNG], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['PSS', SNG], ['OUTNOISE', SNG], ['INPUTRES', SNG], ['COMPLIANCE', SNG], ['MAXLOAD', SNG], ['ILIMIT', SNG], ['PERCOVER', SNG], ['MINVS', SNG], ['MAXVS', SNG], ]; // DSCT: 121 bytes/record (uses INTEGER for some fields) const DSCT_FIELDS = [ ['MODNAME', S14], ['SENTYPE', S7], ['MININ', SNG], ['MAXIN', SNG], ['IEXCMFS', SNG], ['IEXCPFS', SNG], ['RCONV', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', INT], ['ATTEN', INT], ['ATTENTOL', INT], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['OUTNOISE', SNG], ['INPUTRES', SNG], ['IOPENTC', SNG], ['LEADRERR', SNG], ['LINEARIZED', INT], ['OSCALIN', SNG], ['GNCALIN', SNG], ['BANDWIDTH', SNG], ['IMATCHTOL', SNG], ['CALTOL', SNG], ['VSEN', SNG], ]; const S9 = 'STRING9'; // SCM5B45: 119 bytes/record (frequency/counter modules) const SCM5B45_FIELDS = [ ['MODNAME', S9], ['MININ', SNG], ['MAXIN', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['ZHYSAMPL', SNG], ['ZHYSLIM', SNG], ['TTLHYSAMPL', SNG], ['TTLLIMHI', SNG], ['TTLLIMLO', SNG], ['MINPW', SNG], ['OSCALIN', SNG], ['GNCALIN', SNG], ['CALTOL', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', INT], ['ATTEN', INT], ['ATTENTOL', INT], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['ISMAX', SNG], ['PSS', SNG], ['NOISEMFS', SNG], ['NOISETESTPT', SNG], ['NOISEPFS', SNG], ['OUTRES', SNG], ['EXCVOLT', SNG], ['EXCTOLNL', SNG], ['EXCTOLL', SNG], ]; // SCM5B48: 264 bytes/record (multi-bandwidth modules) const SCM5B48_FIELDS = [ ['MODNAME', S15], ['SENTYPE', S7], ['ISMAXNEXCL', SNG], ['ISMAXFEXCL', SNG], ['OUTRES', SNG], ['MININ', SNG], ['MAXIN', SNG], ['MININ1', SNG], ['MAXIN1', SNG], ['MININ2', SNG], ['MAXIN2', SNG], ['MININ3', SNG], ['MAXIN3', SNG], ['IEXC', SNG], ['IEXC1', SNG], ['IEXC2', SNG], ['RCONV', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['CALTOL', SNG], ['VEXC', SNG], ['VEXCACC', SNG], ['EXCLOAD', SNG], ['EXCLOADREG', SNG], ['EXCIMAX', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', SNG], ['TESTFREQ1', SNG], ['TESTFREQ2', SNG], ['TESTFREQ3', SNG], ['TESTFREQ4', SNG], ['ATTEN', SNG], ['ATTEN1', SNG], ['ATTEN2', SNG], ['ATTEN3', SNG], ['ATTEN4', SNG], ['ATTENTOL', SNG], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['PSS', SNG], ['PSS1', SNG], ['PSS2', SNG], ['PSS3', SNG], ['OUTNOISE', SNG], ['OUTNOISE1', SNG], ['OUTNOISE2', SNG], ['OUTNOISE3', SNG], ['INPUTRES', SNG], ['VOPENINMIN', SNG], ['VOPENINMAX', SNG], ['LEADRERR', SNG], ['LINEARIZED', INT], ['OSCALIN', SNG], ['GNCALIN', SNG], ['BANDWIDTH', SNG], ['BANDWIDTH1', SNG], ['BANDWIDTH2', SNG], ['BANDWIDTH3', SNG], ['BANDWIDTH4', SNG], ['IMATCHTOL', SNG], ]; // SCM5B49: 93 bytes/record (sample & hold modules) const SCM5B49_FIELDS = [ ['MODNAME', S9], ['MININ', SNG], ['MAXIN', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['MAXSUPPLYNL', SNG], ['MAXSUPPLYFL', SNG], ['LIMITOUT', SNG], ['POWERSEN', SNG], ['TESTFREQ', INT], ['ATTEN', INT], ['LINEAR0MA', SNG], ['LINEAR50MA', SNG], ['ACCURACY0MA', SNG], ['ACCURACY50MA', SNG], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['NOISEOUT', SNG], ['QINJECT', SNG], ['INPUTRES', SNG], ['ACQLIM', SNG], ['DROOP', SNG], ['PERCOVER', SNG], ]; // DSCA (TSTDIN1B variant, for DSCMAIN4.DAT): 159 bytes/record const DSCA_DIN_FIELDS = [ ['MODNAME', S13], ['SENTYPE', S7], ['ISMAXNEXCL', SNG], ['ISMAXFEXCL', SNG], ['MININ', SNG], ['MAXIN', SNG], ['IEXCPFS', SNG], ['IEXCMFS', SNG], ['RCONV', SNG], ['OUTSIGTYPE', S7], ['MINOUT', SNG], ['MAXOUT', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['CALTOL', SNG], ['VEXC', SNG], ['VEXCACC', SNG], ['EXCLOAD', SNG], ['EXCLOADREG', SNG], ['EXCIMAX', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['TESTFREQ', INT], ['ATTEN', INT], ['ATTENTOL', INT], ['STEPRMIN', SNG], ['STEPRMAX', SNG], ['PSS', SNG], ['OUTNOISE', SNG], ['INPUTRES', SNG], ['OPENTC', SNG], ['LEADRERR', SNG], ['LINEARIZED', INT], ['OSCALIN', SNG], ['GNCALIN', SNG], ['BANDWIDTH', SNG], ['MINVS', SNG], ['MAXVS', SNG], ]; // SCM7B: 170 bytes/record const S17 = 'STRING17'; const SCM7B_FIELDS = [ ['MODNAME', S17], ['SENTYPE', S7], ['MINVS', SNG], ['NOMVS', SNG], ['MAXVS', SNG], ['VLIM', SNG], ['ILIM', SNG], ['PE', SNG], ['ISMAXNEXCL', SNG], ['MININ', SNG], ['MAXIN', SNG], ['MINOUT', SNG], ['MAXOUT', SNG], ['IEXC', SNG], ['EXCIMIN', SNG], ['EXCIMAX', SNG], ['LEADRERR', SNG], ['RCONV', SNG], ['OSCALPT', SNG], ['GNCALPT', SNG], ['CALTOL', SNG], ['ISMAXFEXCL', SNG], ['VEXC', SNG], ['VEXCLO', SNG], ['VEXCHI', SNG], ['LOOPIMAX', SNG], ['LINEAR', SNG], ['ACCURACY', SNG], ['PSS', SNG], ['TESTFREQ', INT], ['ATTEN', INT], ['ATTENTOL', INT], ['STEPRESP', SNG], ['STEPTOL', SNG], ['OUTNOISERMS', SNG], ['OUTNOISEVPK', SNG], ['INPUTRES', SNG], ['VOPENTC', SNG], ['CJCACC', SNG], ['IBIAS', SNG], ]; // -------------------------------------------------------------------------- // Record size calculation // -------------------------------------------------------------------------- function calcRecordSize(fields) { let size = 0; for (const [, type] of fields) { size += FIELD_TYPES[type].size; } return size; } // -------------------------------------------------------------------------- // Parse a single record from a buffer // -------------------------------------------------------------------------- function parseRecord(buf, offset, fields) { const record = {}; let pos = offset; for (const [name, type] of fields) { const ft = FIELD_TYPES[type]; record[name] = ft.read(buf, pos); pos += ft.size; } return record; } // -------------------------------------------------------------------------- // Parse an entire DAT file into an array of records // -------------------------------------------------------------------------- function parseDatFile(filePath, fields) { if (!fs.existsSync(filePath)) { console.error(`Spec file not found: ${filePath}`); return []; } const buf = fs.readFileSync(filePath); const recordSize = calcRecordSize(fields); const numRecords = Math.floor(buf.length / recordSize); const records = []; for (let i = 0; i < numRecords; i++) { const offset = i * recordSize; if (offset + recordSize > buf.length) break; const record = parseRecord(buf, offset, fields); // Skip records with empty, placeholder, or corrupted model names const modname = record.MODNAME; if (!modname || modname.length === 0) continue; // Skip if model name contains non-alphanumeric characters (except dash) if (!/^[A-Za-z0-9-]+$/.test(modname)) continue; // Skip placeholder entries if (/^[XYZ]+$/.test(modname) || /^ZZZZ/.test(modname)) continue; // Skip if MODNAME doesn't start with a known product prefix const upper = modname.toUpperCase(); if (!upper.match(/^(SCM5B|5B|SCM7B|7B|8B|DSCA|DSCT|SCT|BOGUS)/)) continue; records.push(record); } return records; } // -------------------------------------------------------------------------- // Family configuration // -------------------------------------------------------------------------- const FAMILIES = { SCM5B: { file: '5BMAIN.DAT', fields: SCM5B_FIELDS, family: 'SCM5B', logType: '5BLOG', }, B8: { file: '8BMAIN.DAT', fields: B8_FIELDS, family: '8B', logType: '8BLOG', }, DSCA: { file: 'DSCOUT.DAT', fields: DSCA_FIELDS, family: 'DSCA', logType: 'DSCLOG', }, DSCT: { file: 'SCTMAIN.DAT', fields: DSCT_FIELDS, family: 'DSCT', logType: 'SCTLOG', }, DSCA_DIN: { file: 'DSCMAIN4.DAT', fields: DSCA_DIN_FIELDS, family: 'DSCA', logType: 'DSCLOG', }, SCM5B45: { file: '5B45DATA.DAT', fields: SCM5B45_FIELDS, family: 'SCM5B', logType: '5BLOG', }, SCM5B48: { file: 'DB5B48.DAT', fields: SCM5B48_FIELDS, family: 'SCM5B', logType: '5BLOG', }, SCM5B49: { file: '5B49_2.DAT', fields: SCM5B49_FIELDS, family: 'SCM5B', logType: '5BLOG', }, SCM7B: { file: '7BMAIN.DAT', fields: SCM7B_FIELDS, family: 'SCM7B', logType: '7BLOG', }, }; // -------------------------------------------------------------------------- // Main API: load all specs into a lookup map // -------------------------------------------------------------------------- /** * Load all model specs from binary DAT files. * @param {string} specDir - Directory containing the DAT files * @returns {Map} Map of model_number -> spec record (with _family added) */ function loadAllSpecs(specDir) { specDir = specDir || DEFAULT_SPEC_DIR; const specMap = new Map(); for (const [familyKey, config] of Object.entries(FAMILIES)) { const filePath = path.join(specDir, config.file); const records = parseDatFile(filePath, config.fields); for (const record of records) { record._family = config.family; record._logType = config.logType; // Normalize model name for lookup (trim, uppercase) const key = record.MODNAME.toUpperCase().trim(); specMap.set(key, record); } console.log(`[SPEC] Loaded ${records.length} models from ${config.file} (${config.family})`); } console.log(`[SPEC] Total models loaded: ${specMap.size}`); return specMap; } /** * Look up specs for a model number. * Tries exact match, then common prefix variations (SCM5B <-> 5B, DSCA <-> DSC). * @param {Map} specMap - Spec map from loadAllSpecs() * @param {string} modelNumber - Model number to look up * @returns {object|null} Spec record or null */ function getSpecs(specMap, modelNumber) { if (!modelNumber) return null; const key = modelNumber.toUpperCase().trim(); // SCMVAS/SCMHVAS/VAS/HVAS are Accuracy-only; no binary spec file exists for them. // Return a sentinel so export-datasheets.js routes them through the SCMVAS template // instead of skipping on "missing specs". if (/^(SCMVAS|SCMHVAS|VAS|HVAS)-/.test(key)) { return { MODNAME: modelNumber.trim(), _family: 'SCMVAS', _noSpecs: true }; } // Exact match if (specMap.has(key)) return specMap.get(key); // Try adding/removing SCM prefix: "5B41-03" <-> "SCM5B41-03" if (key.startsWith('SCM5B')) { const short = key.replace('SCM5B', '5B'); if (specMap.has(short)) return specMap.get(short); } else if (key.startsWith('5B')) { const full = 'SCM' + key; if (specMap.has(full)) return specMap.get(full); } // Try adding/removing SCM prefix for 7B if (key.startsWith('SCM7B')) { const short = key.replace('SCM7B', '7B'); if (specMap.has(short)) return specMap.get(short); } else if (key.startsWith('7B')) { const full = 'SCM' + key; if (specMap.has(full)) return specMap.get(full); } // Try DSCA variations if (key.startsWith('DSCA')) { // Some specs stored without the 'A' const short = key.replace('DSCA', 'DSC'); if (specMap.has(short)) return specMap.get(short); } // Try partial match on model base (before any suffix like C, D) // e.g., "DSCA30-05C" -> try "DSCA30-05" const baseMatch = key.match(/^(.+?)([A-Z])$/); if (baseMatch) { const base = baseMatch[1]; if (specMap.has(base)) return specMap.get(base); // Also try with prefix variations if (base.startsWith('SCM5B')) { const short = base.replace('SCM5B', '5B'); if (specMap.has(short)) return specMap.get(short); } else if (base.startsWith('5B')) { if (specMap.has('SCM' + base)) return specMap.get('SCM' + base); } } return null; } /** * Determine product family from model number string */ function getFamily(modelNumber) { if (!modelNumber) return null; const m = modelNumber.toUpperCase(); // Order matters: SCMHVAS/SCMVAS must match before generic SCM5B-style. if (m.startsWith('SCMHVAS') || m.startsWith('SCMVAS') || m.startsWith('HVAS') || m.startsWith('VAS-')) return 'SCMVAS'; if (m.startsWith('SCM5B') || m.startsWith('5B')) return 'SCM5B'; if (m.startsWith('SCM7B') || m.startsWith('7B')) return 'SCM7B'; if (m.startsWith('8B')) return '8B'; if (m.startsWith('DSCA')) return 'DSCA'; if (m.startsWith('DSCT') || m.startsWith('SCT')) return 'DSCT'; return null; } // -------------------------------------------------------------------------- // CLI: test the parser // -------------------------------------------------------------------------- if (require.main === module) { const specDir = process.argv[2] || DEFAULT_SPEC_DIR; console.log(`Loading specs from: ${specDir}\n`); const specMap = loadAllSpecs(specDir); // Print a few examples from each family const examples = {}; for (const [key, spec] of specMap) { const fam = spec._family; if (!examples[fam]) examples[fam] = []; if (examples[fam].length < 3) { examples[fam].push(spec); } } for (const [fam, specs] of Object.entries(examples)) { console.log(`\n--- ${fam} Examples ---`); for (const s of specs) { console.log(` ${s.MODNAME}: SENTYPE=${s.SENTYPE}, MININ=${s.MININ}, MAXIN=${s.MAXIN}, MINOUT=${s.MINOUT}, MAXOUT=${s.MAXOUT}`); } } } module.exports = { loadAllSpecs, getSpecs, getFamily, FAMILIES };