Cracked the DSCA33/45 accuracy-block numeric formatting against the Hoffman originals
(formatAccuracyLineDSCA3345):
- mA-output models store calc (and, for DSCA45, meas) in AMPS -> x1000 to display mA;
DSCA33 stores meas already in display unit (NOT scaled), DSCA45 scales both.
- DSCA33 (AC-RMS): stim/calc/meas UNSIGNED, error signed; stim is AC input to 3 dp.
- DSCA45 (frequency): stim is an UNSIGNED integer Hz; calc/meas/error SIGNED.
- Math.fround on accuracy values (QB single-precision rounding), matching the Final-Test fix.
Final-Test fixes too: leading-zero drop only when the value overflows QB's 6-char field
("-0.0005"->"-.0005", but "-0.750" keeps its zero); spec-less section sub-heads
(Zero-Crossing Input / TTL Input) render with NO status (only Withstand/Hi-Pot get PASS);
DSCA33 prints a "Check List" header after the underline.
slotmap-from-hoffman.js (new): derive slotMaps for the models the staged multi-unit
derivation couldn't (vintage-heavy) by matching the Hoffman _srcSerial original's
Final-Test measured values (at display precision) to the DB STATUS entries. Recovered
all 13 remaining DSCA33 models.
Validation (validate-dsca3345.js, content-normalized byte-compare vs live Hoffman
originals): 54 of 56 models PASS and are marked validated:true (the render gate).
2 holdouts (DSCA33-04A, DSCA33-1891) each have ONE accuracy cert at a rounding boundary
where fround rounds opposite to the original; left UNvalidated -> still render null
(safe). DSCA33-1948 + DSCA45-1746 (24 units) have no Hoffman original.
Gate now OPEN for the 54 validated models (render live); 2 holdouts + the no-template
pair stay null. Publishing the api_uploaded_at IS NULL gap next (never re-pushes the
~7,157 pristine originals).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1123 lines
48 KiB
JavaScript
1123 lines
48 KiB
JavaScript
/**
|
|
* Exact-Match Datasheet Formatter
|
|
*
|
|
* Generates TXT datasheets matching the original QuickBASIC DATASHEETWRITE output.
|
|
* Requires a DB record (with raw_data) and model specs from spec-reader.
|
|
*/
|
|
|
|
const { getFamily } = require('../parsers/spec-reader');
|
|
|
|
// DSCA per-model Final-Test templates (Fix 2 STAGE 1 output). Each entry is
|
|
// { accOut: 'Output (V)'|'Output (mA)', rows: [{name, spec}, ...] }, extracted
|
|
// byte-accurately from the staged originals. This is the AUTHORITATIVE source
|
|
// of DSCA parameter names + specs + accuracy output label; loaded once.
|
|
let DSCA_TEMPLATES = {};
|
|
try {
|
|
DSCA_TEMPLATES = require('../dsca-templates.json');
|
|
} catch (e) {
|
|
DSCA_TEMPLATES = {};
|
|
}
|
|
|
|
// DSCA33/DSCA45 templates recovered from the Hoffman API (their main spec files were
|
|
// lost in the wipe). Superset schema: also carries a verbatim 2-line `accHeader`, a
|
|
// `_srcSerial` validation oracle, and a `validated` gate flag. A model renders ONLY
|
|
// after its render byte-matches the Hoffman original (validated:true) — until then it
|
|
// stays null/skipped so the pipeline never overwrites a pristine original.
|
|
let DSCA3345_TEMPLATES = {};
|
|
try {
|
|
DSCA3345_TEMPLATES = require('../dsca33-45-templates.json');
|
|
} catch (e) {
|
|
DSCA3345_TEMPLATES = {};
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// DATA LINES: parameter names and units per family
|
|
// -------------------------------------------------------------------------
|
|
|
|
const DATA_LINES = {
|
|
SCM5B: [
|
|
['Supply Current, Nom', 'mA'], // 1
|
|
['Supply Current, Max', 'mA'], // 2
|
|
['Exc. Current #1', 'uA'], // 3
|
|
['Exc. Current #2', 'uA'], // 4
|
|
['Exc. Current Match', 'uA'], // 5
|
|
['Output Resistance', 'ohms'], // 6
|
|
['CJC Gain', 'uV/C'], // 7
|
|
['Exc. Voltage', 'V'], // 8
|
|
['Exc. Load Reg.', 'ppm/mA'], // 9
|
|
['Vout Reg. w/ Load', '%'], // 10
|
|
['Exc. Current Limit', 'mA'], // 11
|
|
['Linearity', '%'], // 12
|
|
['Accuracy', '%'], // 13
|
|
['Lead R Effect', 'C/ohm'], // 14
|
|
['Supply Sensitivity', 'uV/%'], // 15
|
|
['Input Resistance', 'Mohms'], // 16
|
|
['Open Input Response', 'V'], // 17
|
|
['Frequency Response', 'dB'], // 18
|
|
['Step Response', '%'], // 19
|
|
['Output Noise', 'uVrms'], // 20
|
|
['Over-range Response', 'V'], // 21
|
|
],
|
|
'8B': [
|
|
['Supply Current, Nom', 'mA'],
|
|
['Supply Current, Max', 'mA'],
|
|
['Exc. Current #1', 'uA'],
|
|
['Exc. Current #2', 'uA'],
|
|
['Exc. Current Match', 'uA'],
|
|
['Output Resistance', 'ohms'],
|
|
['CJC Gain', 'uV/C'],
|
|
['Exc. Voltage', 'V'],
|
|
['Exc. Load Reg.', 'ppm/mA'],
|
|
['Vout Reg. w/ Load', '%'],
|
|
['Exc. Current Limit', 'mA'],
|
|
['Linearity', '%'],
|
|
['Accuracy', '%'],
|
|
['Lead R Effect', 'C/ohm'],
|
|
['Supply Sensitivity', 'ppm/%'],
|
|
['Input Resistance', 'Mohms'],
|
|
['Open Input Response', 'V'],
|
|
['Frequency Response', 'dB'],
|
|
['Step Response', '%'],
|
|
['Output Noise', 'uVrms'],
|
|
['Over-range Response', 'V'],
|
|
],
|
|
DSCA: [
|
|
['Supply Current, Nom', 'mA'],
|
|
['Supply Current @ Max Load', 'mA'],
|
|
['Linearity, 0mA Load', '%'],
|
|
['Accuracy, 0mA Load', '%'],
|
|
['Linearity, 5mA Load', '%'],
|
|
['Accuracy, 5mA Load', '%'],
|
|
['Linearity, 50mA Load', '%'],
|
|
['Accuracy, 50mA Load', '%'],
|
|
['Positive Current Limit', 'mA'],
|
|
['Negative Current Limit', 'mA'],
|
|
['Overrange', '%'],
|
|
['Power Supply Sensitivity', '%/%'],
|
|
['Input Resistance', 'Mohms'],
|
|
['Frequency Response', 'dB'],
|
|
['Step Response', '%'],
|
|
['Output Noise', ''],
|
|
['Compliance', '%'],
|
|
['Accuracy @ 5 ohm load', '%'],
|
|
],
|
|
SCM7B: [
|
|
['Supply Current', 'mA'], // 1
|
|
['Supply Current w/ Load', 'mA'], // 2
|
|
['Bias Current', 'nA'], // 3
|
|
['Input Resistance', 'kohms'], // 4
|
|
['Offset Calibration', 'mV'], // 5
|
|
['Gain Calibration', 'mV'], // 6
|
|
['Linearity/Conformity', '%'], // 7
|
|
['Accuracy', '%'], // 8
|
|
['VLoop @ 0 mA (Vs = 18V)', 'V'], // 9
|
|
[' (Vs = 35V)', 'V'], // 10
|
|
['VLoop @ 4 mA (Vs = 18V)', 'V'], // 11
|
|
[' (Vs = 35V)', 'V'], // 12
|
|
['VLoop @ 20mA (Vs = 18V)', 'V'], // 13
|
|
[' (Vs = 35V)', 'V'], // 14
|
|
['VLoop Peak Ripple', 'mV'], // 15
|
|
['High Excitation Current', 'uA'], // 16
|
|
['Low Excitation Current', 'uA'], // 17
|
|
['Output Effective Power', 'mW'], // 18
|
|
['Supply Sensitivity', '%/%Vs'], // 19
|
|
['Open Sensor Response', 'V'], // 20
|
|
['Lead Resistance Effect', 'C/ohm'], // 21
|
|
['CJC Gain', 'uV/C'], // 22
|
|
['100kHz Output Noise', 'uVrms'], // 23
|
|
['Attenuation', 'dB'], // 24
|
|
['150ms Step Response', 'V'], // 25
|
|
['Output Noise', 'mVpk'], // 26
|
|
['Over-Range', 'V'], // 27
|
|
['Under-Range', 'V'], // 28
|
|
['Open Loop Detect', 'mA'], // 29
|
|
['Error @ Max Rload', '%'], // 30
|
|
['Pass-Through Error', '%'], // 31
|
|
],
|
|
SCMVAS: [
|
|
['Accuracy', '%'],
|
|
],
|
|
DSCT: [
|
|
['Under-range Limit', 'mA'],
|
|
['Over-range Limit', 'mA'],
|
|
['Error @ Vloop = 10.8V', '%'],
|
|
['Error @ Vloop = 60V', '%'],
|
|
['Minus f.s. Exc. Current', 'uA'],
|
|
['Plus f.s. Exc. Current', 'uA'],
|
|
['Current Source Matching', '%'],
|
|
['Linearity / Conformity', '%'],
|
|
['Accuracy', '%'],
|
|
['Lead Resistance Effects', 'C/ohm'],
|
|
['Loop Voltage Sensitivity', '%/V'],
|
|
['Input Resistance', 'Mohm'],
|
|
['Open Thermocouple Response', 'mA'],
|
|
['Frequency Response', 'dB'],
|
|
['Step Response', '%'],
|
|
['Output Noise', 'uArms'],
|
|
],
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Sensor type number mapping (for input column headers)
|
|
// -------------------------------------------------------------------------
|
|
|
|
function getSensorNum(sentype) {
|
|
if (!sentype) return 1;
|
|
const s = sentype.toUpperCase().trim();
|
|
if (s === 'V' || s === 'MV') return 1;
|
|
if (s === 'MA') return 2;
|
|
if (s.includes('JTC') || s === 'J') return 3;
|
|
if (s.includes('KTC') || s === 'K') return 4;
|
|
if (s.includes('TTC') || s === 'T') return 5;
|
|
if (s.includes('ETC') || s === 'E' || s.includes('RTC') || s.includes('STC') || s.includes('NTC') || s.includes('BTC')) return 6;
|
|
if (s.includes('RTD')) return 7;
|
|
if (s === 'FBRIDGE' || s === 'HBRIDGE') return 8;
|
|
if (s === '2WTX') return 9;
|
|
return 1; // default voltage
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Parse raw_data from DB record
|
|
// -------------------------------------------------------------------------
|
|
|
|
function parseRawData(rawData, family) {
|
|
if (!rawData) return null;
|
|
|
|
const lines = rawData.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
if (lines.length < 8) return null;
|
|
|
|
const result = {
|
|
modelLine: '',
|
|
accuracy: [], // 5 points: { stim, calc, meas, error, status }
|
|
stepResponse: 0,
|
|
statusEntries: [],
|
|
};
|
|
|
|
let lineIdx = 0;
|
|
|
|
// Line 0: model name (quoted)
|
|
result.modelLine = lines[lineIdx++].replace(/"/g, '').trim();
|
|
|
|
// Lines 1-5: accuracy points
|
|
for (let i = 0; i < 5 && lineIdx < lines.length; i++) {
|
|
const parts = parseCSVLine(lines[lineIdx++]);
|
|
if (parts.length >= 5) {
|
|
result.accuracy.push({
|
|
stim: parseFloat(parts[0]),
|
|
calc: parseFloat(parts[1]),
|
|
meas: parseFloat(parts[2]),
|
|
error: parseFloat(parts[3]),
|
|
status: parts[4].replace(/"/g, '').trim(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Next line: step response / placeholders.
|
|
// SCM5B/8B: "0","0",value DSCT: just value. Many DSCA models OMIT this bare
|
|
// line and go straight to the STATUS groups; consuming a STATUS group here
|
|
// drops a Final-Test row (the "lines drop" defect). For DSCA, skip consuming
|
|
// when the line is actually a STATUS group (starts with PASS/FAIL).
|
|
if (lineIdx < lines.length) {
|
|
const looksLikeStatus = /^"?(PASS|FAIL)/i.test(lines[lineIdx].trim());
|
|
if (!(family === 'DSCA' && looksLikeStatus)) {
|
|
const parts = parseCSVLine(lines[lineIdx++]);
|
|
const lastVal = parts[parts.length - 1];
|
|
result.stepResponse = parseFloat(lastVal) || 0;
|
|
}
|
|
}
|
|
|
|
// Remaining lines: STATUS groups
|
|
// SCM5B/8B: groups of 5, DSCT: groups of 4
|
|
const groupSize = (family === 'DSCT') ? 4 : 5;
|
|
while (lineIdx < lines.length) {
|
|
const line = lines[lineIdx];
|
|
// Stop if we hit the serial/date line
|
|
if (line.match(/^"\d+-\d+[A-Za-z]?","/)) break;
|
|
const parts = parseCSVLine(line);
|
|
for (const p of parts) {
|
|
result.statusEntries.push(p.replace(/"/g, ''));
|
|
}
|
|
lineIdx++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Simple CSV parser that handles quoted strings
|
|
function parseCSVLine(line) {
|
|
const parts = [];
|
|
let current = '';
|
|
let inQuotes = false;
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const ch = line[i];
|
|
if (ch === '"') {
|
|
inQuotes = !inQuotes;
|
|
} else if (ch === ',' && !inQuotes) {
|
|
parts.push(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += ch;
|
|
}
|
|
}
|
|
parts.push(current.trim());
|
|
return parts;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Format measured value from STATUS entry
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a number matching QuickBASIC STR$() behavior:
|
|
* - Positive numbers get a leading space
|
|
* - Leading zeros before decimal are dropped (0.03 -> .03)
|
|
* - Rounds to 6 significant digits to clean IEEE 754 artifacts
|
|
*/
|
|
function r(val, fixedDecimals) {
|
|
if (val == null || isNaN(val)) return '0';
|
|
const rounded = parseFloat(val.toPrecision(6));
|
|
let str;
|
|
if (fixedDecimals != null) {
|
|
str = rounded.toFixed(fixedDecimals);
|
|
} else {
|
|
str = String(rounded);
|
|
}
|
|
// QB STR$() drops leading zero: "0.03" -> ".03"
|
|
str = str.replace(/^0\./, '.').replace(/^-0\./, '-.');
|
|
// QB STR$() prepends space for positive numbers
|
|
if (rounded >= 0 && !str.startsWith(' ')) {
|
|
str = ' ' + str;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* Parse STATUS$ entry and format measured value matching QB PRINT USING.
|
|
* QB format strings all produce exactly 6 characters for the number:
|
|
* "0" -> "###### &" (integer, 6 digits)
|
|
* "1" -> "####.# &" (1 decimal, 6 chars)
|
|
* "2" -> "####.# &" (same as 1)
|
|
* "3" -> "##.### &" (3 decimals, 6 chars)
|
|
* "4" -> "#.#### &" (4 decimals, 6 chars)
|
|
*/
|
|
function formatMeasured(statusStr) {
|
|
if (!statusStr || statusStr.length <= 4) return null;
|
|
|
|
const passFail = statusStr.substring(0, 4); // "PASS" or "FAIL"
|
|
const decimalDigit = statusStr[statusStr.length - 1];
|
|
const valueStr = statusStr.substring(5, statusStr.length - 1).trim();
|
|
const value = parseFloat(valueStr);
|
|
|
|
if (isNaN(value)) return { passFail, formatted: valueStr, width: 6 };
|
|
|
|
// QB PRINT USING: right-justified in 6 character positions
|
|
// Negative sign takes one digit position
|
|
let formatted;
|
|
switch (decimalDigit) {
|
|
case '0': formatted = Math.round(value).toString().padStart(6); break;
|
|
case '1': formatted = value.toFixed(1).padStart(6); break;
|
|
case '2': formatted = value.toFixed(1).padStart(6); break;
|
|
case '3': formatted = value.toFixed(3).padStart(6); break;
|
|
case '4': formatted = value.toFixed(4).padStart(6); break;
|
|
default: formatted = value.toFixed(1).padStart(6); break;
|
|
}
|
|
|
|
return { passFail, formatted, value };
|
|
}
|
|
|
|
/**
|
|
* DSCA measured-value formatter. The DSCA Final-Test STATUS$ entries use the
|
|
* trailing digit as a literal decimal-place count (code N -> toFixed(N)),
|
|
* UNLIKE the 5B/8B QB format strings where code 2 means 1 decimal. Returns the
|
|
* trimmed value string (caller column-aligns it) plus the PASS/FAIL prefix.
|
|
*/
|
|
function formatMeasuredExact(statusStr) {
|
|
if (!statusStr || statusStr.length <= 4) return null;
|
|
const passFail = statusStr.substring(0, 4);
|
|
const decimalDigit = statusStr[statusStr.length - 1];
|
|
// char at index 4 is either a space (positive) or '-' (negative); start there
|
|
// so negative signs survive (e.g. "PASS-4.2424060" -> "-4", not "4").
|
|
const valueStr = statusStr.substring(4, statusStr.length - 1).trim();
|
|
const parsed = parseFloat(valueStr);
|
|
if (isNaN(parsed)) return { passFail, valStr: valueStr, value: NaN };
|
|
// The DOS QuickBASIC stored/computed these as single-precision floats, so the
|
|
// value at the half-rounding boundary rounds the way single precision rounds,
|
|
// not double. Recover the single (Math.fround) before formatting — without it,
|
|
// double-precision toFixed flips last-digit boundaries (e.g. 9.9995 -> "9.999"
|
|
// here but "10.000" in the original; 46.85 -> "46.9" vs "46.8").
|
|
const value = Math.fround(parsed);
|
|
const d = parseInt(decimalDigit, 10);
|
|
const valStr = isNaN(d) ? value.toFixed(1) : value.toFixed(d);
|
|
return { passFail, valStr, value };
|
|
}
|
|
|
|
/**
|
|
* Split a DSCA template spec string into its value part and trailing unit.
|
|
* e.g. "< 30 mA" -> { valuePart: "< 30", unit: "mA" } (internal spacing kept,
|
|
* so the value part can be right-aligned to match the staged column layout).
|
|
* "+/- 11 ppm/mA" -> { valuePart: "+/- 11", unit: "ppm/mA" }.
|
|
*/
|
|
function splitSpecUnit(spec) {
|
|
const s = String(spec);
|
|
const m = s.match(/^(.*\S)\s+(\S+)$/);
|
|
if (m) return { valuePart: m[1], unit: m[2] };
|
|
return { valuePart: s.trim(), unit: '' };
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Format TSPEC display string from spec values
|
|
// -------------------------------------------------------------------------
|
|
|
|
function buildTSpecs(specs, family, stepResponse) {
|
|
if (!specs) return [];
|
|
const tspecs = [];
|
|
|
|
if (family === 'SCM5B' || family === '8B') {
|
|
tspecs[1] = ' < ' + r(specs.ISMAXNEXCL);
|
|
tspecs[2] = ' < ' + r(specs.ISMAXFEXCL);
|
|
tspecs[3] = ' ' + r(specs.IEXC);
|
|
tspecs[4] = ' ' + r(specs.IEXC);
|
|
const imatchtol = (specs.IMATCHTOL || 0) / 100;
|
|
tspecs[5] = '+/-' + r(specs.IEXC * imatchtol, 0);
|
|
tspecs[6] = family === '8B' ? ' < 50' : ' < ' + r(specs.OUTRES || 55);
|
|
tspecs[7] = ''; // CJC gain - computed from polynomial, skip for now
|
|
if (specs.VEXC) {
|
|
const vexcAcc = Math.round(specs.VEXCACC / 100 * specs.VEXC * 1000) / 1000;
|
|
tspecs[8] = r(specs.VEXC, 1) + '+/-' + r(vexcAcc, 3);
|
|
} else {
|
|
tspecs[8] = '';
|
|
}
|
|
tspecs[9] = '+/-' + r(specs.EXCLOADREG);
|
|
const acc125 = Math.round((specs.ACCURACY * 1.25) * 100) / 100;
|
|
tspecs[10] = '+/-' + r(acc125);
|
|
tspecs[11] = ' < ' + r(specs.EXCIMAX);
|
|
tspecs[12] = '+/-' + r(specs.LINEAR);
|
|
tspecs[13] = '+/-' + r(specs.ACCURACY);
|
|
tspecs[14] = '+/-' + r(stepResponse || 0, 1);
|
|
tspecs[15] = '+/-' + r(specs.PSS || 0);
|
|
tspecs[16] = ' >=' + r(specs.INPUTRES);
|
|
if (specs.VOPENINMIN != null && specs.VOPENINMAX != null) {
|
|
tspecs[17] = r(specs.VOPENINMIN, 2) + ' to ' + r(specs.VOPENINMAX, 2);
|
|
} else {
|
|
tspecs[17] = '';
|
|
}
|
|
tspecs[18] = r(specs.ATTEN) + '+/-' + r(specs.ATTENTOL);
|
|
tspecs[19] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
|
|
tspecs[20] = ' < ' + r(specs.OUTNOISE);
|
|
tspecs[21] = tspecs[17]; // duplicate
|
|
} else if (family === 'DSCA') {
|
|
tspecs[1] = ' < ' + r(specs.ISMAXNL || 0);
|
|
tspecs[2] = ' < ' + r(specs.ISMAXFL || 0);
|
|
tspecs[3] = '+/-' + r(specs.LINEAR1 || 0);
|
|
tspecs[4] = '+/-' + r(specs.ACCURACY1 || 0);
|
|
tspecs[5] = '+/-' + r(specs.LINEAR2 || 0);
|
|
tspecs[6] = '+/-' + r(specs.ACCURACY2 || 0);
|
|
tspecs[7] = '+/-' + r(specs.LINEAR3 || 0);
|
|
tspecs[8] = '+/-' + r(specs.ACCURACY3 || 0);
|
|
tspecs[9] = ' < ' + r(specs.ILIMIT || 0);
|
|
tspecs[10] = ' > ' + r(-(specs.ILIMIT || 0));
|
|
tspecs[11] = ' > ' + r(specs.PERCOVER || 0);
|
|
tspecs[12] = '+/-' + r(specs.PSS || 0);
|
|
tspecs[13] = ' >=' + r(specs.INPUTRES || 0);
|
|
tspecs[14] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
|
|
tspecs[15] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
|
|
tspecs[16] = ' <=' + r(specs.OUTNOISE || 0);
|
|
tspecs[17] = '+/-' + r(specs.COMPLIANCE || 0);
|
|
tspecs[18] = '+/-' + r((specs.ACCURACY1 || 0) * 2);
|
|
} else if (family === 'DSCT') {
|
|
tspecs[1] = ''; // computed at runtime
|
|
tspecs[2] = ''; // computed at runtime
|
|
tspecs[3] = ' < 1';
|
|
tspecs[4] = ' < 1';
|
|
const iexcmTol = specs.MODNAME && specs.MODNAME.startsWith('DSCT') ? 0.05 : 0.02;
|
|
tspecs[5] = Math.round(specs.IEXCMFS || 0) + '+/-' + Math.round((specs.IEXCMFS || 0) * iexcmTol);
|
|
tspecs[6] = Math.round(specs.IEXCPFS || 0) + '+/-' + Math.round((specs.IEXCPFS || 0) * iexcmTol);
|
|
tspecs[7] = '+/-' + r(specs.IMATCHTOL || 0);
|
|
tspecs[8] = '+/- ' + r(specs.LINEAR || 0);
|
|
tspecs[9] = '+/- ' + r(specs.ACCURACY || 0);
|
|
tspecs[10] = '+/-' + r(stepResponse || 0, 1);
|
|
tspecs[11] = '+/-' + r(specs.VSEN || 0);
|
|
tspecs[12] = ' >=' + r(specs.INPUTRES || 0);
|
|
const iopentc = specs.IOPENTC || 0;
|
|
const maxout = specs.MAXOUT || 20;
|
|
tspecs[13] = (iopentc > maxout ? ' > ' : ' < ') + r(iopentc);
|
|
tspecs[14] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
|
|
tspecs[15] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
|
|
tspecs[16] = ' < ' + r(specs.OUTNOISE || 0);
|
|
} else if (family === 'SCM7B') {
|
|
const orange = (specs.MAXOUT || 5) - (specs.MINOUT || 0);
|
|
tspecs[1] = '< ' + r(specs.ISMAXNEXCL + 6);
|
|
tspecs[2] = '< ' + r(specs.ISMAXFEXCL + 6);
|
|
tspecs[3] = '+/-' + r(specs.IBIAS || 0);
|
|
tspecs[4] = ' > ' + r(specs.INPUTRES || 0);
|
|
const calTol = 20 * orange * (specs.CALTOL || 0);
|
|
tspecs[5] = '+/-' + r(calTol);
|
|
tspecs[6] = '+/-' + r(calTol);
|
|
tspecs[7] = '+/-' + r(specs.LINEAR || 0);
|
|
tspecs[8] = '+/-' + r(specs.ACCURACY || 0);
|
|
if (specs.VEXC) {
|
|
const vexc5 = specs.VEXC * 0.05;
|
|
tspecs[9] = r(specs.VEXC) + ' +/-' + r(vexc5);
|
|
tspecs[10] = tspecs[9];
|
|
}
|
|
if (specs.VEXCLO) {
|
|
const vlo5 = specs.VEXCLO * 0.05;
|
|
tspecs[11] = r(specs.VEXCLO) + ' +/-' + r(vlo5);
|
|
tspecs[12] = tspecs[11];
|
|
}
|
|
if (specs.VEXCHI) {
|
|
const vhi5 = specs.VEXCHI * 0.05;
|
|
tspecs[13] = r(specs.VEXCHI) + ' +/-' + r(vhi5);
|
|
tspecs[14] = tspecs[13];
|
|
}
|
|
tspecs[15] = ' < 50';
|
|
tspecs[16] = ' < ' + r(specs.EXCIMAX || 0);
|
|
tspecs[17] = ' > ' + r(specs.EXCIMIN || 0);
|
|
tspecs[18] = ' > ' + r(specs.PE || 0);
|
|
tspecs[19] = '+/-' + r(specs.PSS || 0);
|
|
tspecs[20] = ''; // Open TC - needs runtime calc
|
|
tspecs[21] = '+/-' + r(specs.LEADRERR || 0);
|
|
tspecs[22] = ''; // CJC - needs seebeck polynomial
|
|
tspecs[23] = ' < ' + r(specs.OUTNOISERMS || 0);
|
|
tspecs[24] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
|
|
// Step response
|
|
if (specs.STEPRESP && specs.STEPTOL) {
|
|
const lowV = specs.STEPRESP - specs.STEPTOL;
|
|
const highV = specs.STEPRESP + specs.STEPTOL;
|
|
tspecs[25] = r(lowV) + ' to ' + r(highV);
|
|
} else {
|
|
tspecs[25] = '';
|
|
}
|
|
tspecs[26] = ' < ' + r(specs.OUTNOISEVPK || 0);
|
|
tspecs[27] = '+5 to +5.8';
|
|
tspecs[28] = '-.9 to +1';
|
|
tspecs[29] = '0';
|
|
tspecs[30] = ''; // Compliance - needs runtime calc
|
|
tspecs[31] = '+/-' + r(specs.ACCURACY || 0);
|
|
}
|
|
|
|
return tspecs;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Format accuracy value based on sensor type
|
|
// -------------------------------------------------------------------------
|
|
|
|
function formatAccuracyLine(point, sensorNum, maxIn) {
|
|
let stimStr;
|
|
if ((sensorNum >= 3 && sensorNum <= 6) || sensorNum === 7) {
|
|
// Temperature: +####.##
|
|
stimStr = formatSigned(point.stim, 2, 8);
|
|
} else {
|
|
// Voltage/Current: +###.###
|
|
const scale = (maxIn != null && maxIn < 1) ? 1000 : 1;
|
|
stimStr = formatSigned(point.stim * scale, 3, 8);
|
|
}
|
|
|
|
const calcStr = formatSigned(point.calc, 3, 7);
|
|
const measStr = formatSigned(point.meas, 3, 7);
|
|
const errorStr = formatSigned(point.error, 3, 8);
|
|
|
|
return ' ' + stimStr + ' ' + calcStr + ' ' + measStr + ' ' + errorStr + ' ' + point.status;
|
|
}
|
|
|
|
/**
|
|
* Accuracy row for the Hoffman-mined DSCA33/DSCA45 families, whose original software
|
|
* used different column conventions than the voltage/temp models (verified against the
|
|
* Hoffman originals):
|
|
* - mA-output models store calc (and, for DSCA45, meas) in AMPS -> x1000 to display mA.
|
|
* DSCA33 stores meas already in the display unit (NOT scaled); DSCA45 scales both.
|
|
* - DSCA33 (AC-RMS) prints stim/calc/meas UNSIGNED (error signed); stim is the AC input
|
|
* to 3 decimals.
|
|
* - DSCA45 (frequency input) prints stim as an UNSIGNED integer Hz; calc/meas/error SIGNED.
|
|
*/
|
|
function formatAccuracyLineDSCA3345(point, model, accOut) {
|
|
const scale = /mA/.test(accOut || '') ? 1000 : 1;
|
|
const isDSCA45 = /^DSCA45/i.test((model || '').trim());
|
|
// values were computed in QB single precision; recover the single before formatting
|
|
// so last-digit rounding at the .5 boundary matches the original (Math.fround).
|
|
const num = (val, decimals, signed) => ((signed && val >= 0) ? '+' : '') + Math.fround(val).toFixed(decimals);
|
|
let stimStr, calcStr, measStr;
|
|
if (isDSCA45) {
|
|
stimStr = num(point.stim, 0, false).padStart(8);
|
|
calcStr = num(point.calc * scale, 3, true).padStart(7);
|
|
measStr = num(point.meas * scale, 3, true).padStart(7);
|
|
} else {
|
|
stimStr = num(point.stim, 3, false).padStart(8);
|
|
calcStr = num(point.calc * scale, 3, false).padStart(7);
|
|
measStr = num(point.meas, 3, false).padStart(7);
|
|
}
|
|
const errorStr = num(point.error, 3, true).padStart(8);
|
|
return ' ' + stimStr + ' ' + calcStr + ' ' + measStr + ' ' + errorStr + ' ' + point.status;
|
|
}
|
|
|
|
/**
|
|
* Set text at a specific column position (0-indexed) in a string.
|
|
* Pads with spaces if the string is shorter than the target column.
|
|
*/
|
|
function setCol(str, col, text) {
|
|
while (str.length < col) str += ' ';
|
|
return str + text;
|
|
}
|
|
|
|
/**
|
|
* Pad string to reach a column position (for inline TAB simulation).
|
|
* Returns spaces needed to reach the column from current position.
|
|
*/
|
|
function padToCol(str, col) {
|
|
const needed = col - str.length;
|
|
return needed > 0 ? ' '.repeat(needed) : ' ';
|
|
}
|
|
|
|
function formatSigned(val, decimals, width) {
|
|
const sign = val >= 0 ? '+' : '';
|
|
const str = sign + val.toFixed(decimals);
|
|
return str.padStart(width);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Main: generate exact-match TXT datasheet
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Generate an exact-match TXT datasheet from a DB record and model specs.
|
|
* @param {object} record - DB record with raw_data, model_number, serial_number, test_date
|
|
* @param {object} specs - Model spec record from spec-reader
|
|
* @returns {string|null} Formatted TXT datasheet, or null if data is insufficient
|
|
*/
|
|
function generateExactDatasheet(record, specs) {
|
|
const family = getFamily(record.model_number);
|
|
if (!family) return null;
|
|
|
|
if (family === 'SCMVAS') {
|
|
return generateSCMVASDatasheet(record);
|
|
}
|
|
|
|
// DSCA: the per-model template is the authoritative Final-Test layout (names +
|
|
// specs + accuracy label). Source is the staged-original set (dsca-templates) or
|
|
// the Hoffman-mined set (dsca33-45-templates) for the families whose specs were
|
|
// lost. No template -> do not guess; skip this cert.
|
|
const dscaKey = (record.model_number || '').trim();
|
|
// Hoffman-mined templates take PRECEDENCE: DSCA33/45 were also captured by the
|
|
// STAGE 1 staged extractor (sometimes with accOut "?" and no accHeader), and that
|
|
// stale entry must not shadow the authoritative Hoffman-mined one.
|
|
const dscaTpl = (family === 'DSCA')
|
|
? (DSCA3345_TEMPLATES[dscaKey] || DSCA_TEMPLATES[dscaKey] || null)
|
|
: null;
|
|
if (family === 'DSCA' && !dscaTpl) return null;
|
|
// Hoffman-mined DSCA33/45 render only once the model is byte-validated against its
|
|
// Hoffman original — otherwise stay null so an unverified render can't overwrite a
|
|
// live original. The validation harness sets DSCA_VALIDATE_MODE to render
|
|
// unvalidated models for the byte-compare; the live service never sets it.
|
|
if (family === 'DSCA' && DSCA3345_TEMPLATES[dscaKey] && !DSCA3345_TEMPLATES[dscaKey].validated
|
|
&& !process.env.DSCA_VALIDATE_MODE) return null;
|
|
|
|
const parsed = (family === 'SCM7B')
|
|
? parse7BRawData(record.raw_data)
|
|
: parseRawData(record.raw_data, family);
|
|
if (!parsed) return null;
|
|
if (family !== 'SCM7B' && parsed.accuracy.length < 5) return null;
|
|
|
|
const dataLines = DATA_LINES[family];
|
|
if (!dataLines) return null;
|
|
|
|
const sentype = specs ? specs.SENTYPE : '';
|
|
const sensorNum = getSensorNum(sentype);
|
|
const maxIn = specs ? specs.MAXIN : 10;
|
|
const tspecs = specs ? buildTSpecs(specs, family, parsed.stepResponse) : [];
|
|
|
|
// Format test date from YYYY-MM-DD to MM-DD-YYYY
|
|
const dateParts = (record.test_date || '').split('-');
|
|
const dateStr = dateParts.length === 3
|
|
? `${dateParts[1]}-${dateParts[2]}-${dateParts[0]}`
|
|
: record.test_date || '';
|
|
|
|
let modelName = specs ? specs.MODNAME : record.model_number;
|
|
// 7B header prepends "SCM" to the model name
|
|
if (family === 'SCM7B' && !modelName.toUpperCase().startsWith('SCM')) {
|
|
modelName = 'SCM' + modelName;
|
|
}
|
|
|
|
const lines = [];
|
|
const TAB5 = ' '; // 4 spaces = TAB(5) in QB (0-indexed)
|
|
|
|
// ---- Header ----
|
|
lines.push(TAB5 + 'DATAFORTH CORPORATION Phone: (520) 741-1404');
|
|
lines.push(TAB5 + '3331 E. Hemisphere Loop Fax: (520) 741-0762');
|
|
lines.push(TAB5 + 'Tucson, AZ 85706 USA email: info@dataforth.com');
|
|
lines.push('');
|
|
lines.push(' TEST DATA SHEET');
|
|
lines.push(TAB5 + '~'.repeat(71));
|
|
// QB: PRINT #9, TAB(5); "Date: "; DATE$
|
|
// PRINT #9, TAB(5); "Model: "; SPECS.MODNAME
|
|
// PRINT #9, TAB(5); "SN: "; TAB(12); SN$
|
|
lines.push(TAB5 + 'Date: ' + dateStr);
|
|
lines.push(TAB5 + 'Model: ' + modelName);
|
|
let snLine = TAB5 + 'SN: ';
|
|
snLine = setCol(snLine, 11, record.serial_number); // TAB(12) = index 11
|
|
lines.push(snLine);
|
|
lines.push('');
|
|
|
|
// ---- Accuracy Test ----
|
|
// 7B CSV format doesn't include individual accuracy test points (only error pcts in LOGIT)
|
|
// The accuracy data is only in the SHT files, not the DAT files
|
|
if (family === 'SCM7B') {
|
|
// Skip accuracy section entirely for 7B — data not available from DAT format
|
|
} else {
|
|
lines.push(' ACCURACY TEST');
|
|
lines.push('');
|
|
if (dscaTpl && Array.isArray(dscaTpl.accHeader) && dscaTpl.accHeader.length >= 2) {
|
|
// DSCA33/45 (Hoffman-mined): the accuracy header carries model-specific tokens
|
|
// the sensor-type logic can't synthesize (Vin (mVAC), Iin (AAC), Frequency (Hz),
|
|
// Output (VDC)/(mADC)). Emit the verbatim 2-line header from the original.
|
|
lines.push(dscaTpl.accHeader[0]);
|
|
lines.push(dscaTpl.accHeader[1]);
|
|
lines.push(TAB5 + '-'.repeat(10) + ' ' + '-'.repeat(11) + ' ' + '-'.repeat(11) + ' ' + '-'.repeat(10) + ' ' + '-'.repeat(8));
|
|
} else {
|
|
lines.push(' Calculated Measured');
|
|
|
|
// Input column header based on sensor type
|
|
let inputHeader;
|
|
if ((sensorNum >= 3 && sensorNum <= 6) || sensorNum === 7) {
|
|
inputHeader = ' Temp. (C)';
|
|
} else if (sensorNum === 2 || sensorNum === 9) {
|
|
inputHeader = ' Iin (mA)';
|
|
} else {
|
|
inputHeader = (maxIn != null && maxIn < 1) ? ' Vin (mV)' : ' Vin (V)';
|
|
}
|
|
// DSCA labels its accuracy output column "Output (V)"/"Output (mA)" (from the
|
|
// template) with '-' rule separators; 5B/8B/etc. use "Vout (V)" with '='.
|
|
const accOut = (family === 'DSCA' && dscaTpl) ? dscaTpl.accOut : 'Vout (V)';
|
|
const accSep = (family === 'DSCA') ? '-' : '=';
|
|
lines.push(' ' + inputHeader + ' ' + accOut + ' ' + accOut + '* Error (%) Status');
|
|
lines.push(TAB5 + accSep.repeat(10) + ' ' + accSep.repeat(10) + ' ' + accSep.repeat(10) + ' ' + accSep.repeat(9) + ' ' + accSep.repeat(8));
|
|
}
|
|
|
|
for (const point of parsed.accuracy) {
|
|
if (dscaTpl && Array.isArray(dscaTpl.accHeader)) {
|
|
lines.push(formatAccuracyLineDSCA3345(point, record.model_number, dscaTpl.accOut));
|
|
continue;
|
|
}
|
|
lines.push(formatAccuracyLine(point, sensorNum, maxIn));
|
|
}
|
|
lines.push('');
|
|
} // end accuracy section conditional
|
|
|
|
// ---- Final Test Results ----
|
|
// QB column positions (1-indexed): TAB(31), TAB(47), TAB(60-speclen), TAB(61), TAB(71)
|
|
lines.push(' FINAL TEST RESULTS');
|
|
lines.push('');
|
|
if (family === 'DSCA') {
|
|
// DSCA Final-Test renders from the per-model staged template: the rows give
|
|
// the parameter names + specs (and accuracy label) directly; the value-bearing
|
|
// raw_data STATUS groups map positionally onto the spec-bearing rows. Rows with
|
|
// an empty spec (240VAC Withstand / Hi-Pot) carry no measured value and render
|
|
// as PASS. Header/column scheme matches the staged originals.
|
|
|
|
// Value-bearing measurements in source order (drop "PASS"/"" padding entries).
|
|
const measurements = [];
|
|
for (const s of parsed.statusEntries) {
|
|
const m = formatMeasuredExact(s);
|
|
if (m) measurements.push(m);
|
|
}
|
|
const specRowCount = dscaTpl.rows.filter(r => (r.spec || '').trim()).length;
|
|
// The simple positional zip is sound only when there is exactly one measured
|
|
// value per spec-bearing row. When counts differ, this subtype measures slots
|
|
// the template omits (e.g. an extra load pair); use the per-model slotMap
|
|
// (absolute statusEntries index per spec-bearing row, derived from the staged
|
|
// originals) to pull the right value. With no usable slotMap, skip rather than
|
|
// misalign ("do not guess").
|
|
const useSlot = (measurements.length !== specRowCount)
|
|
&& Array.isArray(dscaTpl.slotMap) && dscaTpl.slotMap.length === specRowCount;
|
|
if (measurements.length !== specRowCount && !useSlot) return null;
|
|
|
|
let h1 = setCol('', 12, 'Parameter');
|
|
h1 = setCol(h1, 31, 'Measured Value*');
|
|
h1 = setCol(h1, 51, 'Specification');
|
|
h1 = setCol(h1, 69, 'Status');
|
|
lines.push(h1);
|
|
let h2 = setCol('', 4, '='.repeat(25));
|
|
h2 = setCol(h2, 31, '='.repeat(15));
|
|
h2 = setCol(h2, 48, '='.repeat(19));
|
|
h2 = setCol(h2, 69, '='.repeat(6));
|
|
lines.push(h2);
|
|
|
|
const is3345 = Array.isArray(dscaTpl.accHeader); // Hoffman-mined DSCA33/45
|
|
let mi = 0, si = 0;
|
|
for (const row of dscaTpl.rows) {
|
|
const spec = (row.spec || '').trim();
|
|
let line = setCol('', 4, row.name);
|
|
if (spec) {
|
|
const su = splitSpecUnit(spec);
|
|
const m = useSlot
|
|
? formatMeasuredExact(parsed.statusEntries[dscaTpl.slotMap[si++]])
|
|
: measurements[mi++];
|
|
if (m) {
|
|
// measured value right-justified ending at col 38, unit at col 40.
|
|
// DSCA33/45 follow QB's fixed 6-char number field: a value that would
|
|
// overflow drops its leading zero to fit ("-0.0005" (7) -> "-.0005" (6));
|
|
// values that already fit (e.g. "-0.750", "0.0000") keep it.
|
|
let v = String(m.valStr);
|
|
if (is3345 && v.length > 6) v = v.replace(/^-0\./, '-.');
|
|
line = setCol(line, 39 - v.length, v);
|
|
if (su.unit) line = setCol(line, 40, su.unit);
|
|
}
|
|
// spec value-part right-justified ending at col 58, unit at col 60
|
|
line = setCol(line, 59 - su.valuePart.length, su.valuePart);
|
|
if (su.unit) line = setCol(line, 60, su.unit);
|
|
line = setCol(line, 70, m ? m.passFail : 'PASS');
|
|
} else if (is3345 && !/withstand|hi-?pot/i.test(row.name)) {
|
|
// DSCA33/45 spec-less rows that are NOT a pass/fail test (e.g. the
|
|
// "Zero-Crossing Input" / "TTL Input" section sub-heads) carry no status.
|
|
// (Leave the row as just the name.)
|
|
} else {
|
|
// spec-less pass/fail row (240VAC Withstand / Hi-Pot): blank measured + PASS
|
|
line = setCol(line, 70, 'PASS');
|
|
}
|
|
lines.push(line);
|
|
}
|
|
// Footer load note ("Standard output load for test is ... ohms.") — printed
|
|
// before the underline, only by the models whose staged original had it
|
|
// (captured per-model in STAGE 1; not all current-output models print it).
|
|
if (dscaTpl.loadNote) {
|
|
lines.push('');
|
|
lines.push(TAB5 + dscaTpl.loadNote);
|
|
}
|
|
} else {
|
|
// QB: TAB(12); "Parameter"; TAB(30); "Measured Value"; TAB(51); "Specification "; TAB(70); "Status"
|
|
let hdr1 = setCol('', 11, 'Parameter');
|
|
hdr1 = setCol(hdr1, 29, 'Measured Value');
|
|
hdr1 = setCol(hdr1, 50, 'Specification ');
|
|
hdr1 = setCol(hdr1, 69, 'Status');
|
|
lines.push(hdr1);
|
|
// QB: TAB(5); "======================="; TAB(30); "==============="; TAB(47); "====================="; TAB(70); "======"
|
|
let hdr2 = setCol('', 4, '=======================');
|
|
hdr2 = setCol(hdr2, 29, '===============');
|
|
hdr2 = setCol(hdr2, 46, '=====================');
|
|
hdr2 = setCol(hdr2, 69, '======');
|
|
lines.push(hdr2);
|
|
|
|
for (let i = 0; i < dataLines.length && i < parsed.statusEntries.length; i++) {
|
|
const status = parsed.statusEntries[i];
|
|
if (!status || status.length <= 4) continue; // Skip if no measured data
|
|
|
|
const [paramName, paramUnit] = dataLines[i];
|
|
let unit = paramUnit;
|
|
|
|
// Unit overrides per QB logic
|
|
if (family === 'SCM5B' || family === '8B') {
|
|
if (i === 13 && sensorNum === 7) unit = 'ohm/ohm';
|
|
if (i === 14 && (sensorNum === 5 || sensorNum === 6)) unit = 'C/V';
|
|
}
|
|
|
|
const measured = formatMeasured(status);
|
|
if (!measured) continue;
|
|
|
|
// Build line matching QB TAB positions (converting to 0-indexed for string ops)
|
|
// TAB(5): parameter name
|
|
// TAB(31): measured value (6 chars right-justified) + space + unit
|
|
// TAB(60-speclen): spec string right-aligned to end at col 60
|
|
// TAB(61): unit
|
|
// TAB(71): PASS/FAIL
|
|
let line = '';
|
|
line = setCol(line, 4, paramName); // TAB(5) = index 4
|
|
line = setCol(line, 30, measured.formatted + ' ' + unit); // TAB(31) = index 30
|
|
|
|
const tspec = tspecs[i + 1]; // 1-indexed in TSPECS
|
|
if (tspec) {
|
|
const specLen = tspec.length;
|
|
line = setCol(line, 59 - specLen, tspec); // TAB(60-speclen)
|
|
line = setCol(line, 60, unit); // TAB(61) = index 60
|
|
}
|
|
line = setCol(line, 70, measured.passFail); // TAB(71) = index 70
|
|
|
|
lines.push(line);
|
|
}
|
|
} // end non-DSCA Final Test Results
|
|
|
|
// ---- Footer ----
|
|
// 240 VAC / Hi-Pot (conditional by family/model)
|
|
if (family === 'SCM5B') {
|
|
const mn = (modelName || '').trim();
|
|
if (!mn.startsWith('SCM5BPT') && !mn.startsWith('SCM5B-1369')) {
|
|
lines.push(TAB5 + '240 VAC Withstand' + ''.padEnd(49) + 'PASS');
|
|
lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
|
|
}
|
|
} else if (family === '8B') {
|
|
const mn = (modelName || '').trim();
|
|
if (!mn.startsWith('8BPT')) {
|
|
lines.push(TAB5 + 'VAC Withstand' + ''.padEnd(53) + 'PASS');
|
|
lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
|
|
}
|
|
} else if (family === 'SCM7B') {
|
|
const mn = (modelName || '').toUpperCase();
|
|
if (!mn.includes('7BPT')) {
|
|
let vac = setCol(TAB5 + '120VAC Withstand', 70, 'PASS');
|
|
lines.push(vac);
|
|
let hp = setCol(TAB5 + 'Hi-Pot', 70, 'PASS');
|
|
lines.push(hp);
|
|
}
|
|
} else if (family === 'DSCT') {
|
|
lines.push(TAB5 + '240 VAC Withstand' + ''.padEnd(49) + 'PASS');
|
|
lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
|
|
}
|
|
|
|
// Underline + Check List
|
|
lines.push(TAB5 + '_'.repeat(71));
|
|
if (family === 'SCM7B') {
|
|
lines.push(' Packing Check List');
|
|
lines.push('');
|
|
lines.push(setCol(TAB5 + 'Module Appearance: _____', 44, 'Mounting Screw: _____'));
|
|
lines.push('');
|
|
lines.push(setCol(TAB5 + 'Pins Straight: _____', 44, 'Module Header: _____'));
|
|
lines.push('');
|
|
lines.push(setCol(TAB5 + 'Tested by: _____________', 44, 'QC: _______________'));
|
|
} else if (/^DSCA33/i.test((record.model_number || '').trim())) {
|
|
// DSCA33 originals print just the centered "Check List" header (no items).
|
|
lines.push(' Check List');
|
|
} else if (family !== 'DSCA') {
|
|
lines.push(' Check List');
|
|
lines.push('');
|
|
lines.push(setCol(TAB5 + 'Module Appearance: __X__', 44, 'Mounting Screw: __X__'));
|
|
lines.push('');
|
|
lines.push(setCol(TAB5 + 'Pins Straight: __X__', 44, 'Module Header: __X__'));
|
|
}
|
|
|
|
lines.push('');
|
|
lines.push(TAB5 + 'It is hereby certified that the above product is in conformance with');
|
|
lines.push(TAB5 + 'all requirements to the extent specified. This product is not');
|
|
lines.push(TAB5 + 'authorized or warranted for use in life support devices and/or systems.');
|
|
lines.push('');
|
|
lines.push(TAB5 + '* NIST traceable calibration certificates support Measured Value data.');
|
|
lines.push(TAB5 + ' Calibration services are available through ANSI/NCSL Z540-1 and');
|
|
lines.push(TAB5 + ' ISO Guide 25 Certified Metrology Labs.');
|
|
lines.push('');
|
|
|
|
return lines.join('\r\n');
|
|
}
|
|
|
|
/**
|
|
* Parse 7B raw_data (single CSV line format)
|
|
* Format: STAGE: MODEL,SN,DATE,VERSION,DMMSERIAL,val1,...val31,err1,...errN
|
|
* val=9999 means not tested, [val] means FAIL
|
|
*/
|
|
function parse7BRawData(rawData) {
|
|
if (!rawData) return null;
|
|
|
|
const match = rawData.match(/^([A-Z-]+):\s*(.*)$/);
|
|
if (!match) return null;
|
|
|
|
const parts = match[2].split(',');
|
|
if (parts.length < 36) return null; // model + sn + date + version + dmmserial + 31 values minimum
|
|
|
|
const result = {
|
|
modelLine: parts[0].trim(),
|
|
accuracy: [],
|
|
stepResponse: 0,
|
|
statusEntries: [],
|
|
};
|
|
|
|
// Values start at index 5 (after model, sn, date, version, dmmserial)
|
|
for (let i = 0; i < 31; i++) {
|
|
const rawVal = (parts[5 + i] || '').trim();
|
|
|
|
if (rawVal === '9999' || rawVal === '') {
|
|
// Not tested - push short "PASS" (will be skipped by formatter)
|
|
result.statusEntries.push('PASS');
|
|
} else if (rawVal.startsWith('[')) {
|
|
// FAIL - bracketed value
|
|
const val = rawVal.replace(/[\[\]]/g, '').trim();
|
|
const numVal = parseFloat(val);
|
|
if (isNaN(numVal) || numVal === 0) {
|
|
result.statusEntries.push('FAIL');
|
|
} else {
|
|
const decimals = guessDecimals(numVal);
|
|
result.statusEntries.push('FAIL ' + val + decimals);
|
|
}
|
|
} else {
|
|
// PASS with value
|
|
const numVal = parseFloat(rawVal);
|
|
if (isNaN(numVal)) {
|
|
result.statusEntries.push('PASS');
|
|
} else {
|
|
const decimals = guessDecimals(numVal);
|
|
result.statusEntries.push('PASS ' + rawVal.trim() + decimals);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Error percentages follow the 31 values - these are the accuracy test point errors
|
|
const errorStart = 5 + 31;
|
|
for (let i = errorStart; i < parts.length; i++) {
|
|
const val = parseFloat((parts[i] || '').trim());
|
|
if (!isNaN(val)) {
|
|
result.accuracy.push({
|
|
stim: 0, // Stimulus not stored in 7B CSV format
|
|
calc: 0,
|
|
meas: 0,
|
|
error: val * 100, // Convert fraction to percentage
|
|
status: 'PASS',
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Guess the decimal format digit based on value magnitude
|
|
*/
|
|
function guessDecimals(val) {
|
|
const abs = Math.abs(val);
|
|
if (abs === 0) return '0';
|
|
if (abs >= 100) return '0';
|
|
if (abs >= 10) return '1';
|
|
if (abs >= 1) return '1';
|
|
if (abs >= 0.1) return '3';
|
|
return '4';
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// SCMVAS / SCMHVAS: Accuracy-only datasheet (no spec lookup)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// QB's STR$() emits SINGLE values in two formats depending on magnitude:
|
|
// (1) scientific with a trailing test-status digit: "PASS-7.005501E-033"
|
|
// (the trailing single digit is a status code, dropped)
|
|
// (2) plain decimal without status digit: "PASS .01599373" or "PASS-.00499773"
|
|
// Both are already in percent units (not fractions). Try scientific first,
|
|
// then plain-decimal as fallback.
|
|
const SCMVAS_ACCURACY_RE_SCI = /^(PASS|FAIL)\s*(-?\d+\.?\d*E[+-]?\d{2})\d?$/i;
|
|
const SCMVAS_ACCURACY_RE_PLAIN = /^(PASS|FAIL)\s*(-?\.?\d+\.?\d*)$/i;
|
|
|
|
function extractSCMVASAccuracy(rawData) {
|
|
if (!rawData) return null;
|
|
// Scan every quoted string in raw_data for a PASS/FAIL + float value.
|
|
// raw_data lines look like: "PASS-7.005501E-033","","","" — so we extract
|
|
// each quoted token and test it against the regex.
|
|
const tokens = rawData.match(/"[^"]*"/g) || [];
|
|
for (const tok of tokens) {
|
|
const inner = tok.slice(1, -1).trim();
|
|
if (!inner) continue;
|
|
const m = inner.match(SCMVAS_ACCURACY_RE_SCI) || inner.match(SCMVAS_ACCURACY_RE_PLAIN);
|
|
if (m) {
|
|
const passFail = m[1].toUpperCase();
|
|
const value = parseFloat(m[2]);
|
|
if (isNaN(value)) return null;
|
|
return { passFail, value };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function formatSCMVASAccuracyDisplay(value) {
|
|
const abs = Math.abs(value);
|
|
let str = abs.toFixed(3);
|
|
// Trim trailing zeros after decimal, but preserve at least one digit.
|
|
if (str.indexOf('.') >= 0) {
|
|
str = str.replace(/0+$/, '').replace(/\.$/, '');
|
|
}
|
|
return str + '%';
|
|
}
|
|
|
|
function formatSCMVASDate(testDate) {
|
|
if (!testDate) return '';
|
|
// Accept YYYY-MM-DD (DB), MM-DD-YYYY or MM/DD/YYYY (raw). Normalize to MM/DD/YYYY.
|
|
const s = String(testDate).trim();
|
|
let m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
if (m) return `${m[2]}/${m[3]}/${m[1]}`;
|
|
m = s.match(/^(\d{2})[-/](\d{2})[-/](\d{4})$/);
|
|
if (m) return `${m[1]}/${m[2]}/${m[3]}`;
|
|
return s;
|
|
}
|
|
|
|
function generateSCMVASDatasheet(record) {
|
|
const acc = extractSCMVASAccuracy(record.raw_data);
|
|
if (!acc) return null;
|
|
|
|
const TAB8 = ' ';
|
|
const modelName = (record.model_number || '').trim();
|
|
const sn = (record.serial_number || '').trim();
|
|
const dateStr = formatSCMVASDate(record.test_date);
|
|
const measured = formatSCMVASAccuracyDisplay(acc.value);
|
|
const status = acc.passFail;
|
|
|
|
const lines = [];
|
|
|
|
// Header
|
|
lines.push(TAB8 + 'Dataforth Corporation Phone number: (520) 741-1404');
|
|
lines.push(TAB8 + '3331 E. Hemisphere Loop Fax: (520) 741-0762');
|
|
lines.push(TAB8 + 'Tucson, AZ 85706 USA Email: info@dataforth.com');
|
|
lines.push('');
|
|
lines.push('');
|
|
lines.push('');
|
|
lines.push('');
|
|
lines.push(' TEST DATA SHEET');
|
|
lines.push(TAB8 + '~'.repeat(71));
|
|
lines.push(TAB8 + 'Date: ' + dateStr);
|
|
lines.push(TAB8 + 'Model: ' + modelName);
|
|
lines.push(TAB8 + 'SN: ' + sn);
|
|
// Section header: centered "FINAL TEST RESULTS" padded to column 77 to match golden samples.
|
|
lines.push(' FINAL TEST RESULTS ');
|
|
lines.push(TAB8 + '~'.repeat(71));
|
|
|
|
// Results table: columns at 8, 28, 48, 68
|
|
let hdr = TAB8 + 'Parameter';
|
|
hdr = setCol(hdr, 28, 'Measured Value');
|
|
hdr = setCol(hdr, 48, 'Specification');
|
|
hdr = setCol(hdr, 68, 'Status');
|
|
lines.push(hdr);
|
|
|
|
let sep = TAB8 + '================';
|
|
sep = setCol(sep, 28, '==============');
|
|
sep = setCol(sep, 48, '=============');
|
|
sep = setCol(sep, 68, '======');
|
|
lines.push(sep);
|
|
|
|
let row = TAB8 + 'Accuracy';
|
|
row = setCol(row, 28, measured);
|
|
row = setCol(row, 48, '+/- 0.03%');
|
|
row = setCol(row, 68, status);
|
|
lines.push(row);
|
|
|
|
lines.push(TAB8);
|
|
lines.push(TAB8 + '_'.repeat(71));
|
|
lines.push(' Check List');
|
|
lines.push('');
|
|
lines.push(setCol(TAB8 + 'Module Appearance: __X__', 48, 'Mounting Screw: __X__'));
|
|
lines.push('');
|
|
lines.push(setCol(TAB8 + 'Pins Straight: __X__', 48, 'Module Header: __X__'));
|
|
lines.push('');
|
|
lines.push(TAB8 + 'It is hereby certified that the above product is in conformance with');
|
|
lines.push(TAB8 + 'all requirements to the extent specified. This product is not');
|
|
lines.push(TAB8 + 'authorized or warranted for use in life support devices and/or systems.');
|
|
lines.push('');
|
|
lines.push(TAB8 + '* NIST traceable calibration certificates support Measured Value data.');
|
|
lines.push(TAB8 + 'Calibration services are available through ANSI/NCSL Z540-1 and');
|
|
lines.push(TAB8 + 'ISO Guide 25 Certified Metrology Labs.');
|
|
lines.push(TAB8);
|
|
lines.push(TAB8);
|
|
|
|
return lines.join('\r\n');
|
|
}
|
|
|
|
/**
|
|
* True if the model renders from a template that does NOT require spec-reader specs
|
|
* (the Hoffman-mined DSCA33/45 set). Lets render-datasheet.js skip the missing-specs
|
|
* bail for these. (Still gated on per-model `validated` inside generateExactDatasheet.)
|
|
*/
|
|
function rendersWithoutSpecs(modelNumber) {
|
|
return !!DSCA3345_TEMPLATES[(modelNumber || '').trim()];
|
|
}
|
|
|
|
module.exports = {
|
|
generateExactDatasheet,
|
|
generateSCMVASDatasheet,
|
|
extractSCMVASAccuracy,
|
|
parseRawData,
|
|
parse7BRawData,
|
|
rendersWithoutSpecs,
|
|
DATA_LINES,
|
|
};
|