Deployed file is C:\Shares\testdatadb\templates\datasheet-exact.js; this
reconciles the repo copy + adds dsca-templates.json (STAGE 1 output).
What changed in generateExactDatasheet (DSCA family only; 5B/8B/7B/DSCT/SCMVAS
paths byte-unchanged):
- Load dsca-templates.json once at module top (126 per-model layouts).
- DSCA Final-Test now renders names + specs from the staged template rows, not
the single hardcoded DATA_LINES['DSCA'] + buildTSpecs DSCA branch.
- Value-bearing raw_data STATUS groups map positionally onto the spec-bearing
template rows; empty-spec rows (240VAC Withstand / Hi-Pot) render blank+PASS.
Removed the duplicate hardcoded 240VAC/Hi-Pot footer for DSCA (now rows).
- ACCURACY header uses the template accOut ("Output (V)"/"Output (mA)") with '-'
rule separators instead of "Vout (V)" + '='.
- Header/columns match the staged originals (Measured Value*, 25/15/19/6 rule).
Two real bugs fixed (both are the handoff's "lines drop" / wrong-value defect):
- formatMeasuredExact reads the value from index 4 so negative signs survive
("PASS-4.24..." -> "-4", not "4"); also decimal-code N -> toFixed(N) exactly
(DSCA differs from 5B/8B where code 2 means 1 decimal).
- parseRawData no longer consumes the first DSCA STATUS group as a bare
step-response line when that line is absent (dropped 3 rows on e.g. DSCA39-01).
Safety: when value count != spec-row count the positional zip is ambiguous
(subtype measures load points the template omits, e.g. DSCA49 5mA pair), so the
cert is SKIPPED (null) and left for STAGE 3 per-subtype mapping rather than
emitting misaligned data.
Validation: DSCA38-05 (SN 180224-1) Final-Test block byte-identical to its
staged original. 92/126 templated models render cleanly; 7 ambiguous + 27
no-spec skip. Remaining ACCURACY-block spacing diffs are the deferred cosmetic
gap. NOT YET LIVE: testdatadb service not restarted, nothing re-pushed to
Hoffman (STAGE 3 gate).
Coverage gap to resolve before publish: only 126/357 DSCA models in the DB have
a staged template (56,074 certs, 70.1%); 231 models / 23,866 certs have none and
now render null — needs a STAGE 1 extension (more staged originals).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1022 lines
42 KiB
JavaScript
1022 lines
42 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 = {};
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 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 value = parseFloat(valueStr);
|
|
if (isNaN(value)) return { passFail, valStr: valueStr, value: NaN };
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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 STAGE 2: the per-model staged template is the authoritative Final-Test
|
|
// layout (names + specs + accuracy output label). No template means no staged
|
|
// original was available -> do not guess the layout; skip this cert.
|
|
const dscaTpl = (family === 'DSCA')
|
|
? (DSCA_TEMPLATES[(record.model_number || '').trim()] || null)
|
|
: null;
|
|
if (family === 'DSCA' && !dscaTpl) 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('');
|
|
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) {
|
|
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 only sound when there is exactly one measured
|
|
// value per spec-bearing row. When counts differ, this subtype uses a slot
|
|
// layout the zip can't resolve (e.g. raw_data records load points the template
|
|
// omits), so emitting would misalign values onto wrong rows. Skip and flag for
|
|
// STAGE 3 per-subtype mapping rather than publish wrong data ("do not guess").
|
|
if (measurements.length !== specRowCount) 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);
|
|
|
|
let mi = 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 = measurements[mi++];
|
|
if (m) {
|
|
// measured value right-justified ending at col 38, unit at col 40
|
|
const v = String(m.valStr);
|
|
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 {
|
|
// no spec => 240VAC Withstand / Hi-Pot style row: blank measured + PASS
|
|
line = setCol(line, 70, 'PASS');
|
|
}
|
|
lines.push(line);
|
|
}
|
|
} 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 (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__'));
|
|
}
|
|
|
|
// DSCA current output load note
|
|
if (family === 'DSCA' && specs && specs.OUTSIGTYPE && specs.OUTSIGTYPE.trim().toUpperCase() === 'CURRENT') {
|
|
lines.push(TAB5 + 'Standard output load for test is 250 ohms.');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
module.exports = {
|
|
generateExactDatasheet,
|
|
generateSCMVASDatasheet,
|
|
extractSCMVASAccuracy,
|
|
parseRawData,
|
|
parse7BRawData,
|
|
DATA_LINES,
|
|
};
|