repo split: move projects to their own repos as submodules; bulk data purged to Gitea-Storage (Jupiter)
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user