repo split: move projects to their own repos as submodules; bulk data purged to Gitea-Storage (Jupiter)

This commit is contained in:
2026-06-18 19:02:53 -07:00
parent d5bfe76780
commit 4fcd3088af
1266 changed files with 80 additions and 198742 deletions

View File

@@ -1,72 +0,0 @@
/**
* Parser for single-line CSV format (7BLOG)
*
* Format:
* STAGE: MODEL,SERIAL,DATE,VERSION,CODE,VALUE1,VALUE2,...
* Example:
* FINAL: 7B21,87876-1,05-08-2013,1.984,0651945, 12, 9999, ...
*/
const fs = require('fs');
const path = require('path');
/**
* Parse a 7BLOG CSV file and extract test records
* @param {string} filePath - Path to the DAT file
* @param {string} testStation - Test station identifier
* @returns {Array} Array of parsed records
*/
function parseCsvFile(filePath, testStation = null) {
const records = [];
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n').map(l => l.trim());
for (const line of lines) {
if (!line) continue;
// Match pattern: STAGE: MODEL,SERIAL,DATE,...
const match = line.match(/^([A-Z-]+):\s*([^,]+),([^,]+),(\d{2}-\d{2}-\d{4}),(.*)$/);
if (match) {
const [, stage, model, serial, dateStr, rest] = match;
// Parse date from MM-DD-YYYY to YYYY-MM-DD
const [month, day, year] = dateStr.split('-');
const testDate = `${year}-${month}-${day}`;
// Model number includes the stage prefix for 7B products
const modelNumber = model.trim();
records.push({
log_type: '7BLOG',
model_number: modelNumber,
serial_number: serial.trim(),
test_date: testDate,
test_station: testStation,
overall_result: 'PASS', // 7BLOG entries are typically passing records
raw_data: line,
source_file: filePath
});
}
}
} catch (err) {
console.error(`Error parsing ${filePath}: ${err.message}`);
}
return records;
}
/**
* Extract test station from file path
*/
function extractTestStation(filePath) {
const match = filePath.match(/TS-\d+[LR]/i);
return match ? match[0].toUpperCase() : null;
}
module.exports = {
parseCsvFile,
extractTestStation
};

View File

@@ -1,119 +0,0 @@
/**
* Parser for multi-line DAT files (DSCLOG, 5BLOG, 8BLOG, PWRLOG, SCTLOG, VASLOG)
*
* Format:
* "MODEL_NUMBER "
* measurement1,measurement2,measurement3,measurement4,"PASS/FAIL"
* ... (test data lines)
* 0
* "summary line 1"
* ...
* "SERIAL-NUM","MM-DD-YYYY"
*/
const fs = require('fs');
const path = require('path');
/**
* Parse a multi-line DAT file and extract test records
* @param {string} filePath - Path to the DAT file
* @param {string} logType - Type of log (DSCLOG, 5BLOG, etc.)
* @param {string} testStation - Test station identifier (TS-1L, etc.)
* @returns {Array} Array of parsed records
*/
function parseMultilineFile(filePath, logType, testStation = null) {
const records = [];
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n').map(l => l.trim());
let currentRecord = [];
let modelNumber = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip empty lines
if (!line) continue;
// Check if it's a serial/date line (format: "SERIAL","DATE")
const serialDateMatch = line.match(/^"(\d+-\d+[A-Za-z]?)","(\d{2}-\d{2}-\d{4})"$/);
if (serialDateMatch) {
// This is the end of a record
const serialNumber = serialDateMatch[1];
const dateStr = serialDateMatch[2];
if (modelNumber && currentRecord.length > 0) {
// Parse date from MM-DD-YYYY to YYYY-MM-DD
const [month, day, year] = dateStr.split('-');
const testDate = `${year}-${month}-${day}`;
// Determine overall result from raw data
const rawData = currentRecord.join('\n');
const overallResult = determineResult(rawData);
records.push({
log_type: logType,
model_number: modelNumber.trim(),
serial_number: serialNumber,
test_date: testDate,
test_station: testStation,
overall_result: overallResult,
raw_data: rawData,
source_file: filePath
});
}
// Reset for next record
currentRecord = [];
modelNumber = null;
}
// Check if this is a model number line
// Model numbers: single quoted string with product code (letters+numbers, possibly with dash)
// Examples: "DSCA38-1793 ", "SCM5B30-01 ", "8B30-01 "
else if (/^"[A-Z0-9]+[A-Z0-9-]*\s*"$/.test(line) && !line.includes(',') && !line.includes('PASS') && !line.includes('FAIL')) {
// This is a model number line - start new record
if (currentRecord.length > 0 && modelNumber) {
// Previous record didn't have serial/date - skip it
currentRecord = [];
}
modelNumber = line.replace(/"/g, '').trim();
currentRecord.push(line);
} else {
// Add line to current record
currentRecord.push(line);
}
}
} catch (err) {
console.error(`Error parsing ${filePath}: ${err.message}`);
}
return records;
}
/**
* Determine overall PASS/FAIL result from raw data
*/
function determineResult(rawData) {
const failCount = (rawData.match(/"FAIL/gi) || []).length;
const passCount = (rawData.match(/"PASS/gi) || []).length;
if (failCount > 0) return 'FAIL';
if (passCount > 0) return 'PASS';
return 'UNKNOWN';
}
/**
* Extract test station from file path
*/
function extractTestStation(filePath) {
const match = filePath.match(/TS-\d+[LR]/i);
return match ? match[0].toUpperCase() : null;
}
module.exports = {
parseMultilineFile,
extractTestStation
};

View File

@@ -1,96 +0,0 @@
/**
* Parser for SHT (Test Data Sheet) files
*
* Format:
* DATAFORTH CORPORATION ...
* ...
* TEST DATA SHEET
* ~~~~~~~~~~~~~~~~~~~~~~~
* Date: MM-DD-YYYY
* Model: MODEL_NUMBER
* SN: SERIAL-NUM
*
* Parameter Measured Value Specification Status
* ======================= =============== ==================== ======
* Supply Current 12.0 mA < 30 mA PASS
* ...
*/
const fs = require('fs');
const path = require('path');
/**
* Parse an SHT file and extract the test record
* @param {string} filePath - Path to the SHT file
* @param {string} testStation - Test station identifier
* @returns {Array} Array with single parsed record (or empty if parse fails)
*/
function parseShtFile(filePath, testStation = null) {
const records = [];
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let date = null;
let model = null;
let serial = null;
let hasFailure = false;
for (const line of lines) {
// Extract date
const dateMatch = line.match(/^Date:\s*(\d{2}-\d{2}-\d{4})/);
if (dateMatch) {
const [month, day, year] = dateMatch[1].split('-');
date = `${year}-${month}-${day}`;
}
// Extract model
const modelMatch = line.match(/^Model:\s*(\S+)/);
if (modelMatch) {
model = modelMatch[1].trim();
}
// Extract serial number
const snMatch = line.match(/^SN:\s*(\S+)/);
if (snMatch) {
serial = snMatch[1].trim();
}
// Check for FAIL status
if (/\bFAIL\b/i.test(line)) {
hasFailure = true;
}
}
if (date && model && serial) {
records.push({
log_type: 'SHT',
model_number: model,
serial_number: serial,
test_date: date,
test_station: testStation,
overall_result: hasFailure ? 'FAIL' : 'PASS',
raw_data: content,
source_file: filePath
});
}
} catch (err) {
console.error(`Error parsing ${filePath}: ${err.message}`);
}
return records;
}
/**
* Extract test station from file path
*/
function extractTestStation(filePath) {
const match = filePath.match(/TS-\d+[LR]/i);
return match ? match[0].toUpperCase() : null;
}
module.exports = {
parseShtFile,
extractTestStation
};

View File

@@ -1,487 +0,0 @@
/**
* 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<string, object>} 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();
// 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();
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 };

View File

@@ -1,164 +0,0 @@
/**
* Work Order Report Parser
*
* Parses the TXT work order status reports from TS-XX/Reports/ folders.
*
* Format:
* ===================================================================
* WO#: 179257
* Date: 03-27-2026
* Work order status file for work order #: 179257
* Program: TEST8B1D.EXE
* Version: B.19 2023.08.02 JL
* Lib. Ver.: B.09 2019.02.08 MR
* -------------------------------------------------------------------
* Status Serial# DS File Name Model Date Time
* -------- --------- ------------ ------------- ---------- --------
* PASS 179257-1 H9257-1.TXT 8B47K-05 03-27-2026 10:25:56
* FAIL<<<< 179257-12 8B47K-05 03-27-2026 11:01:09
* ...
*/
const fs = require('fs');
const path = require('path');
/**
* Extract test station from file path (e.g., C:\Shares\test\TS-4L\Reports\179257.TXT -> TS-4L)
*/
function extractStation(filePath) {
const match = filePath.match(/[\\\/](TS-[^\\\/]+)[\\\/]/i);
return match ? match[1].toUpperCase() : null;
}
/**
* Extract work order number from filename (e.g., 179257.TXT -> 179257)
*/
function extractWoFromFilename(filePath) {
const base = path.basename(filePath, path.extname(filePath));
return base;
}
/**
* Parse a work order report TXT file
* @param {string} filePath - Path to the report file
* @returns {object} Parsed work order with header and lines
*/
function parseWoReport(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
const result = {
wo_number: null,
wo_date: null,
program: null,
version: null,
lib_version: null,
station: extractStation(filePath),
source_file: filePath,
lines: [], // test result lines
ds_files: [], // datasheet files listed at bottom
};
let inHeader = true;
let inDsList = false;
for (const line of lines) {
const t = line.trim();
// Parse header fields
const woMatch = t.match(/^WO#:\s*(\S+)/);
if (woMatch) {
result.wo_number = woMatch[1];
continue;
}
const dateMatch = t.match(/^Date:\s*(\d{2}-\d{2}-\d{4})/);
if (dateMatch) {
const [month, day, year] = dateMatch[1].split('-');
result.wo_date = `${year}-${month}-${day}`;
continue;
}
const progMatch = t.match(/^Program:\s*(\S+)/);
if (progMatch) {
result.program = progMatch[1];
continue;
}
const verMatch = t.match(/^Version:\s*(.+)/);
if (verMatch) {
result.version = verMatch[1].trim();
continue;
}
const libMatch = t.match(/^Lib\. Ver\.:\s*(.+)/);
if (libMatch) {
result.lib_version = libMatch[1].trim();
continue;
}
// Detect separator lines
if (t.match(/^-{20,}$/)) {
inHeader = false;
continue;
}
// Skip header row
if (t.startsWith('Status') && t.includes('Serial#')) continue;
// Detect datasheet file list section
if (t.includes('datasheet files actually created')) {
inDsList = true;
continue;
}
if (inDsList) {
if (t.match(/^-+$/)) continue;
if (t.match(/^\S+\.TXT$/i)) {
result.ds_files.push(t);
}
continue;
}
// Parse test result lines
// PASS 179257-1 H9257-1.TXT 8B47K-05 03-27-2026 10:25:56
// FAIL<<<< 179257-12 8B47K-05 03-27-2026 11:01:09
if (!inHeader && t.length > 0) {
const passMatch = t.match(/^(PASS)\s+(\S+)\s+(\S+\.TXT)\s+(\S+)\s+(\d{2}-\d{2}-\d{4})\s+(\d{2}:\d{2}:\d{2})/i);
const failMatch = t.match(/^(FAIL[<]*)\s+(\S+)\s+(\S+)\s+(\d{2}-\d{2}-\d{4})\s+(\d{2}:\d{2}:\d{2})/i);
if (passMatch) {
const [, status, serial, dsFile, model, date, time] = passMatch;
const [month, day, year] = date.split('-');
result.lines.push({
status: 'PASS',
serial_number: serial,
ds_filename: dsFile,
model_number: model,
test_date: `${year}-${month}-${day}`,
test_time: time,
});
} else if (failMatch) {
const [, status, serial, model, date, time] = failMatch;
const [month, day, year] = date.split('-');
result.lines.push({
status: 'FAIL',
serial_number: serial,
ds_filename: null,
model_number: model,
test_date: `${year}-${month}-${day}`,
test_time: time,
});
}
}
}
// Fall back to filename for WO# if not found in content
if (!result.wo_number) {
result.wo_number = extractWoFromFilename(filePath);
}
return result;
}
module.exports = { parseWoReport, extractStation, extractWoFromFilename };