feat: FASE 5b/6.1+6.2 - SPC Backend + Dashboard Metrologist (Plotly.js)

Aggiunge servizio SPC con calcoli Cp/Cpk/Pp/Ppk, carta di controllo (UCL/LCL),
istogramma con curva normale. Router FastAPI con 5 endpoint statistics, blueprint
Flask con proxy AJAX, dashboard interattiva Alpine.js + Plotly.js con filtri per
ricetta/subtask/date, riepilogo pass/fail, gauge Cpk e i18n IT/EN completo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 15:00:05 +01:00
parent e1f4ee73d0
commit bcd807e57d
14 changed files with 1567 additions and 242 deletions
+31 -128
View File
@@ -1,142 +1,45 @@
/**
* Caliper Connection Manager
* Web Serial API integration for USB digital calipers
* Supports standard SPC/Digimatic protocol
* Caliper Status - Passive HID Monitor
* USB digital calipers act as keyboard emulators (HID),
* sending digits + Enter as keystrokes. This component
* monitors numpad-confirm events with inputMethod === 'usb_caliper'
* and provides visual feedback (no Web Serial needed).
*/
function caliperConnection() {
function caliperStatus() {
return {
connected: false,
status: 'disconnected', // disconnected, connecting, connected, error
lastReading: null,
port: null,
reader: null,
readController: null,
active: false, // Flash active (for animation)
lastReading: null, // Last value read from caliper
readingCount: 0, // Session reading counter
_flashTimeout: null,
_onConfirm: null,
get isSupported() {
return 'serial' in navigator;
init() {
// Listen for numpad confirmations with usb_caliper method
this._onConfirm = (e) => {
if (e.detail && e.detail.inputMethod === 'usb_caliper') {
this.registerReading(e.detail.value);
}
};
window.addEventListener('numpad-confirm', this._onConfirm);
},
async connect() {
if (!this.isSupported) {
this.status = 'error';
console.error('Web Serial API not supported');
return;
}
registerReading(value) {
this.lastReading = value;
this.readingCount++;
this.active = true;
try {
this.status = 'connecting';
// Request port selection
this.port = await navigator.serial.requestPort();
// Open port with standard caliper settings
await this.port.open({
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: 'none',
flowControl: 'none'
});
this.connected = true;
this.status = 'connected';
// Start reading loop
this.readLoop();
} catch (err) {
this.status = 'error';
this.connected = false;
console.error('Caliper connection error:', err);
}
clearTimeout(this._flashTimeout);
this._flashTimeout = setTimeout(() => {
this.active = false;
}, 1500);
},
async readLoop() {
if (!this.port || !this.port.readable) return;
const reader = this.port.readable.getReader();
this.reader = reader;
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
console.log('Reader closed');
break;
}
this.parseData(value);
}
} catch (err) {
if (err.name !== 'NetworkError') {
console.error('Read error:', err);
}
} finally {
reader.releaseLock();
}
},
parseData(data) {
try {
// Decode buffer to text
const text = new TextDecoder().decode(data);
// Standard digital caliper protocol patterns
// Format: typically " XX.XXX mm\r\n" or similar
// Try multiple patterns for compatibility
// Pattern 1: Standard with units
let match = text.match(/([+-]?\s*\d+\.?\d*)\s*mm/i);
// Pattern 2: Just number with optional sign
if (!match) {
match = text.match(/([+-]?\s*\d+\.?\d*)/);
}
if (match) {
// Clean whitespace and parse
const valueStr = match[1].replace(/\s+/g, '');
const value = parseFloat(valueStr);
if (!isNaN(value)) {
this.lastReading = value;
// Dispatch event for other components
this.$dispatch('caliper-reading', {
value: value,
timestamp: new Date().toISOString()
});
}
}
} catch (err) {
console.error('Parse error:', err);
}
},
async disconnect() {
try {
// Cancel reader
if (this.reader) {
await this.reader.cancel();
this.reader = null;
}
// Close port
if (this.port) {
await this.port.close();
this.port = null;
}
} catch (err) {
console.error('Disconnect error:', err);
} finally {
this.connected = false;
this.status = 'disconnected';
}
},
// Cleanup on component destroy
destroy() {
this.disconnect();
if (this._onConfirm) {
window.removeEventListener('numpad-confirm', this._onConfirm);
}
clearTimeout(this._flashTimeout);
}
};
}