import { useState, useCallback, useRef } from 'react';
import { ArrowLeft, Usb, Database, Copy, RefreshCw, Calendar, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useToast } from '@/hooks/use-toast';
import * as XLSX from 'xlsx';
interface VaisalaReadProps {
onBack: () => void;
}
interface DatabaseRecord {
serienr: string;
ref85: string;
datum: string;
}
// Check if Web Serial API is available
const isWebSerialSupported = 'serial' in navigator;
export function VaisalaRead({ onBack }: VaisalaReadProps) {
const { toast } = useToast();
const portRef = useRef(null);
const readerRef = useRef | null>(null);
// State
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [serialNumber, setSerialNumber] = useState('');
const [calibrationText, setCalibrationText] = useState('');
const [calibrationDate, setCalibrationDate] = useState(() => {
const today = new Date();
return today.toISOString().split('T')[0];
});
const [ref85, setRef85] = useState('');
const [ref85Date, setRef85Date] = useState('');
const [databaseName, setDatabaseName] = useState(null);
const [databaseRecords, setDatabaseRecords] = useState([]);
const [error, setError] = useState(null);
// Connect to serial port
const connectToSensor = useCallback(async () => {
if (!isWebSerialSupported) {
setError('Web Serial API stöds inte i denna webbläsare. Använd Chrome eller Edge.');
return;
}
// Make sure any existing connection is closed first
if (portRef.current) {
try {
await portRef.current.close();
} catch {
// Ignore close errors
}
portRef.current = null;
}
setIsConnecting(true);
setError(null);
try {
// Request port from user
const port = await navigator.serial.requestPort();
// Open with Vaisala settings: 19200 baud, 8 data bits, no parity, 1 stop bit
await port.open({
baudRate: 19200,
dataBits: 8,
parity: 'none',
stopBits: 1,
});
portRef.current = port;
setIsConnected(true);
toast({
title: 'Ansluten',
description: 'Ansluten till givare',
});
} catch (err) {
if (err instanceof Error && err.name === 'NotFoundError') {
// User cancelled port selection
setError('Ingen port vald');
} else if (err instanceof Error && err.message.includes('Failed to open')) {
setError('Kunde inte öppna porten. Stäng eventuella andra program som använder porten (t.ex. Python, Arduino IDE) och försök igen.');
} else {
setError(`Kunde inte ansluta: ${err instanceof Error ? err.message : 'Okänt fel'}`);
}
} finally {
setIsConnecting(false);
}
}, [toast]);
// Disconnect from serial port
const disconnect = useCallback(async () => {
try {
if (readerRef.current) {
await readerRef.current.cancel();
readerRef.current = null;
}
if (portRef.current) {
await portRef.current.close();
portRef.current = null;
}
setIsConnected(false);
setSerialNumber('');
setCalibrationText('');
setRef85('');
setRef85Date('');
} catch (err) {
console.error('Disconnect error:', err);
}
}, []);
// Send command and read response
const sendCommand = useCallback(async (command: string): Promise => {
if (!portRef.current || !portRef.current.writable || !portRef.current.readable) {
throw new Error('Port not available');
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
// Write command
const writer = portRef.current.writable.getWriter();
await writer.write(encoder.encode(command + '\r'));
writer.releaseLock();
// Read response with timeout
const reader = portRef.current.readable.getReader();
readerRef.current = reader;
let response = '';
const timeout = setTimeout(() => {
reader.cancel();
}, 2000);
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
response += decoder.decode(value);
if (response.includes('\r') || response.includes('\n')) break;
}
} finally {
clearTimeout(timeout);
reader.releaseLock();
readerRef.current = null;
}
return response.trim();
}, []);
// Update sensor (read serial, set calibration text and date)
const updateSensor = useCallback(async () => {
if (!portRef.current) {
setError('Ingen givare ansluten');
return;
}
setIsUpdating(true);
setError(null);
try {
// Read serial number
const snumResponse = await sendCommand('SNUM ');
// Parse serial number from response like "SNUM: ABC123"
const snMatch = snumResponse.match(/[^(: )]+$/);
const sn = snMatch ? snMatch[0].replace(/[\r\n]/g, '') : '';
if (!sn) {
throw new Error('Kunde inte läsa serienummer');
}
setSerialNumber(sn);
// Set calibration text
await sendCommand('CTEXT FuktCom AB / Lund');
setCalibrationText('FuktCom AB / Lund');
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 500));
// Set calibration date (format YYYYMMDD)
if (calibrationDate) {
const dateFormatted = calibrationDate.replace(/-/g, '');
await sendCommand(`CDATE ${dateFormatted}`);
}
// Copy serial number to clipboard
await navigator.clipboard.writeText(sn);
// Look up 85% REF from database
const record = databaseRecords.find(r => r.serienr === sn);
if (record) {
setRef85(record.ref85);
setRef85Date(record.datum);
} else {
setRef85('Ej hittad');
setRef85Date('Ej hittad');
}
toast({
title: 'Givare uppdaterad',
description: `Serienummer ${sn} kopierat till urklipp`,
});
} catch (err) {
setError(`Fel vid uppdatering: ${err instanceof Error ? err.message : 'Okänt fel'}`);
} finally {
setIsUpdating(false);
}
}, [calibrationDate, databaseRecords, sendCommand, toast]);
// Copy serial number to clipboard
const copySerialNumber = useCallback(async () => {
if (serialNumber) {
await navigator.clipboard.writeText(serialNumber);
toast({
title: 'Kopierad',
description: `Serienummer ${serialNumber} kopierat till urklipp`,
});
}
}, [serialNumber, toast]);
// Load database Excel file
const handleDatabaseUpload = useCallback((e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = new Uint8Array(event.target?.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// Parse records
const records: DatabaseRecord[] = [];
for (const row of jsonData as Record[]) {
const serienr = String(row['Serienr'] || '').trim();
if (!serienr) continue;
const ref85Val = row['85% Ref.'] || row['85% REF'] || row['85 Ref'] || '';
let datumVal = row['Datum'] || '';
// Format date if it's a number (Excel serial date)
if (typeof datumVal === 'number') {
const date = XLSX.SSF.parse_date_code(datumVal);
datumVal = `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')}`;
} else if (datumVal instanceof Date) {
datumVal = datumVal.toISOString().split('T')[0];
}
records.push({
serienr,
ref85: String(ref85Val),
datum: String(datumVal),
});
}
setDatabaseRecords(records);
setDatabaseName(file.name);
toast({
title: 'Databas laddad',
description: `${records.length} poster hittades`,
});
} catch (err) {
setError(`Fel vid läsning av databas: ${err instanceof Error ? err.message : 'Okänt fel'}`);
}
};
reader.readAsArrayBuffer(file);
}, [toast]);
return (
{/* Header with back button */}
Vaisala Read
Givare GUI
{isConnected ? (
Ansluten
) : null}
{/* Info about iframe limitations */}
Viktigt: För att ansluta till givare måste du öppna appen i ett nytt fönster (inte i förhandsvisningen).
Klicka på "Publish" och öppna den publicerade länken i Chrome eller Edge.
{/* Web Serial API warning */}
{!isWebSerialSupported && (
Web Serial API stöds inte i denna webbläsare. Använd Chrome eller Edge för att ansluta till givare.
)}
{/* Error alert */}
{error && (
{error}
)}