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:
+31
-128
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user