541 lines
18 KiB
JavaScript
541 lines
18 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { changeWeightScalePort, changeWeightScaleWeight, GlobalWeightScalePort } from '../../Features/BookingScreen/BookingData/BookingData';
|
|
import { useDispatch } from 'react-redux';
|
|
import { useSelector } from 'react-redux';
|
|
|
|
const WeightScaleApp = ({ weightscaleclose }) => {
|
|
const dispatch = useDispatch();
|
|
const weightscaleport = useSelector(GlobalWeightScalePort)
|
|
const [weight, setWeight] = useState(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const [serviceRunning, setServiceRunning] = useState(false);
|
|
const [history, setHistory] = useState([]);
|
|
const wsRef = useRef(null);
|
|
const reconnectTimeoutRef = useRef(null);
|
|
const connectionAttempts = useRef(0);
|
|
const shouldReconnect = useRef(true);
|
|
const isConnecting = useRef(false);
|
|
const pollIntervalRef = useRef(null);
|
|
const lastStableWeightRef = useRef(null);
|
|
const WS_URL = 'ws://localhost:8080';
|
|
const CONTROL_URL = 'http://localhost:8081';
|
|
|
|
useEffect(() => {
|
|
// Quick initial check and connect
|
|
checkAndConnect();
|
|
|
|
return () => {
|
|
shouldReconnect.current = false;
|
|
if (wsRef.current) wsRef.current.close();
|
|
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
|
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
|
};
|
|
}, []);
|
|
useEffect(() => {
|
|
if (weightscaleport) {
|
|
startService();
|
|
}
|
|
else{
|
|
stopService();
|
|
}
|
|
}, [weightscaleport]);
|
|
// useEffect(() => {
|
|
// if (!connected) {
|
|
// weightscaleclose(false);
|
|
// }
|
|
|
|
// }, [connected]);
|
|
|
|
|
|
const checkAndConnect = async () => {
|
|
// Quick non-blocking status check
|
|
checkServiceStatus().then(status => {
|
|
if (status?.running) {
|
|
setServiceRunning(true);
|
|
shouldReconnect.current = true;
|
|
// Connect immediately
|
|
setTimeout(() => connectWebSocket(), 50);
|
|
}
|
|
});
|
|
};
|
|
|
|
const checkServiceStatus = async () => {
|
|
try {
|
|
const controller = new AbortController();
|
|
setTimeout(() => controller.abort(), 800); // Fast timeout
|
|
|
|
const response = await fetch(`${CONTROL_URL}/status`, { signal: controller.signal });
|
|
const data = await response.json();
|
|
setServiceRunning(data.running);
|
|
return data;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const openAndroidApp = () => {
|
|
try {
|
|
const scheme = 'pozoprinter';
|
|
const packageName = 'com.example.pozoprinter';
|
|
window.location.href = `${scheme}://#Intent;scheme=${scheme};package=${packageName}NewConfigCheckSWeightScaleNewConfigCheckEServiceConditionSstartServiceConditionE;end;`;
|
|
} catch (e) {
|
|
console.error('Failed to open Android app', e);
|
|
}
|
|
};
|
|
|
|
const startService = async () => {
|
|
// Immediate UI response
|
|
shouldReconnect.current = true;
|
|
|
|
// Clean up immediately
|
|
if (wsRef.current) {
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
}
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
reconnectTimeoutRef.current = null;
|
|
}
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
|
|
isConnecting.current = false;
|
|
connectionAttempts.current = 0;
|
|
|
|
// Quick status check (non-blocking)
|
|
const status = await checkServiceStatus();
|
|
|
|
if (status?.running) {
|
|
// Service already running - connect immediately
|
|
setServiceRunning(true);
|
|
setTimeout(() => connectWebSocket(), 50);
|
|
return;
|
|
}
|
|
|
|
// Try HTTP start (non-blocking, short timeout)
|
|
try {
|
|
const controller = new AbortController();
|
|
setTimeout(() => controller.abort(), 1000);
|
|
|
|
fetch(`${CONTROL_URL}/start`, {
|
|
method: 'POST',
|
|
signal: controller.signal
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
setServiceRunning(true);
|
|
setTimeout(() => connectWebSocket(), 200);
|
|
} else {
|
|
// HTTP failed, open Android app
|
|
openAndroidApp();
|
|
startPolling();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// HTTP failed, open Android app
|
|
openAndroidApp();
|
|
startPolling();
|
|
});
|
|
} catch (error) {
|
|
openAndroidApp();
|
|
startPolling();
|
|
}
|
|
};
|
|
|
|
const startPolling = () => {
|
|
// Quick polling to detect when service starts
|
|
let attempts = 0;
|
|
pollIntervalRef.current = setInterval(async () => {
|
|
attempts++;
|
|
if (attempts > 15) { // 15 seconds max
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const status = await checkServiceStatus();
|
|
if (status?.running) {
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
setServiceRunning(true);
|
|
setTimeout(() => connectWebSocket(), 100);
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const stopService = async () => {
|
|
// Immediate UI response
|
|
shouldReconnect.current = false;
|
|
isConnecting.current = false;
|
|
|
|
// Stop polling immediately
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
|
|
// Clear reconnection
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
reconnectTimeoutRef.current = null;
|
|
}
|
|
|
|
// Close WebSocket immediately
|
|
if (wsRef.current) {
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
}
|
|
|
|
// Update UI immediately
|
|
setConnected(false);
|
|
weightscaleclose(false);
|
|
dispatch(changeWeightScalePort(false));
|
|
setWeight(null);
|
|
dispatch(changeWeightScaleWeight(null));
|
|
setServiceRunning(false);
|
|
connectionAttempts.current = 0;
|
|
|
|
// Send stop request (fire and forget - don't wait)
|
|
try {
|
|
const controller = new AbortController();
|
|
setTimeout(() => controller.abort(), 500);
|
|
fetch(`${CONTROL_URL}/stop`, {
|
|
method: 'POST',
|
|
signal: controller.signal
|
|
}).catch(() => {});
|
|
} catch (error) {}
|
|
};
|
|
|
|
const connectWebSocket = () => {
|
|
if (isConnecting.current || !shouldReconnect.current) return;
|
|
|
|
if (wsRef.current?.readyState === WebSocket.OPEN ||
|
|
wsRef.current?.readyState === WebSocket.CONNECTING) {
|
|
return;
|
|
}
|
|
|
|
if (wsRef.current) {
|
|
try { wsRef.current.close(); } catch (e) {}
|
|
wsRef.current = null;
|
|
}
|
|
|
|
try {
|
|
isConnecting.current = true;
|
|
const ws = new WebSocket(WS_URL);
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
console.log('✅ Connected');
|
|
setConnected(true);
|
|
setServiceRunning(true);
|
|
connectionAttempts.current = 0;
|
|
isConnecting.current = false;
|
|
|
|
if (pollIntervalRef.current) {
|
|
clearInterval(pollIntervalRef.current);
|
|
pollIntervalRef.current = null;
|
|
}
|
|
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
reconnectTimeoutRef.current = null;
|
|
}
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'weight') {
|
|
setWeight(data);
|
|
const w = data?.weight;
|
|
|
|
// Only update global state when the reading is usable
|
|
const isValidNumber = typeof w === 'number' && !Number.isNaN(w);
|
|
|
|
if (
|
|
isValidNumber &&
|
|
data.stable && // <- only when stable
|
|
w !== lastStableWeightRef.current
|
|
) {
|
|
lastStableWeightRef.current = w;
|
|
dispatch(changeWeightScaleWeight(w));
|
|
}
|
|
setHistory(prev => [data, ...prev].slice(0, 10));
|
|
} else if (data.type === 'status') {
|
|
setConnected(data.connected);
|
|
weightscaleclose(data.connected);
|
|
dispatch(changeWeightScalePort(data.connected));
|
|
if (data.running !== undefined) setServiceRunning(data.running);
|
|
}
|
|
} catch (error) {}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
isConnecting.current = false;
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
setConnected(false);
|
|
weightscaleclose(false);
|
|
dispatch(changeWeightScalePort(false));
|
|
wsRef.current = null;
|
|
isConnecting.current = false;
|
|
|
|
if (shouldReconnect.current) {
|
|
connectionAttempts.current++;
|
|
const delay = Math.min(1000 * Math.pow(1.5, connectionAttempts.current), 5000);
|
|
|
|
reconnectTimeoutRef.current = setTimeout(() => {
|
|
if (shouldReconnect.current && !isConnecting.current) {
|
|
connectWebSocket();
|
|
}
|
|
}, delay);
|
|
}
|
|
};
|
|
} catch (error) {
|
|
setConnected(false);
|
|
weightscaleclose(false);
|
|
dispatch(changeWeightScalePort(false));
|
|
wsRef.current = null;
|
|
isConnecting.current = false;
|
|
|
|
if (shouldReconnect.current) {
|
|
setTimeout(() => {
|
|
if (shouldReconnect.current && !isConnecting.current) {
|
|
connectWebSocket();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
};
|
|
|
|
const formatTime = (timestamp) => {
|
|
return new Date(timestamp).toLocaleTimeString();
|
|
};
|
|
|
|
return (
|
|
<></>
|
|
// <div style={{
|
|
// maxWidth: '800px',
|
|
// margin: '0 auto',
|
|
// padding: '20px',
|
|
// fontFamily: 'system-ui, -apple-system, sans-serif'
|
|
// }}>
|
|
// <div style={{
|
|
// background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
// borderRadius: '12px',
|
|
// padding: '24px',
|
|
// color: 'white',
|
|
// marginBottom: '20px',
|
|
// boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
|
|
// }}>
|
|
// <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
|
// <h1 style={{ margin: 0, fontSize: '28px', fontWeight: '600' }}>USB Weight Scale</h1>
|
|
// <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
// <div style={{
|
|
// width: '12px',
|
|
// height: '12px',
|
|
// borderRadius: '50%',
|
|
// backgroundColor: connected ? '#10b981' : '#ef4444',
|
|
// boxShadow: connected ? '0 0 8px #10b981' : '0 0 8px #ef4444'
|
|
// }} />
|
|
// <span style={{ fontSize: '14px', fontWeight: '500' }}>
|
|
// {connected ? 'Connected' : 'Disconnected'}
|
|
// </span>
|
|
// </div>
|
|
// </div>
|
|
// <p style={{ margin: 0, fontSize: '13px', opacity: 0.9 }}>
|
|
// WebSocket: {WS_URL} | Control: {CONTROL_URL}
|
|
// </p>
|
|
// </div>
|
|
|
|
// <div style={{
|
|
// background: 'white',
|
|
// borderRadius: '12px',
|
|
// padding: '24px',
|
|
// marginBottom: '20px',
|
|
// boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
|
// }}>
|
|
// <h2 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600' }}>Service Control</h2>
|
|
// <div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
|
|
// <button
|
|
// onClick={startService}
|
|
// disabled={serviceRunning}
|
|
// style={{
|
|
// flex: 1,
|
|
// padding: '12px 24px',
|
|
// background: serviceRunning ? '#e5e7eb' : 'linear-gradient(135deg, #10b981, #059669)',
|
|
// color: serviceRunning ? '#9ca3af' : 'white',
|
|
// border: 'none',
|
|
// borderRadius: '8px',
|
|
// fontSize: '15px',
|
|
// fontWeight: '500',
|
|
// cursor: serviceRunning ? 'not-allowed' : 'pointer',
|
|
// transition: 'all 0.2s'
|
|
// }}
|
|
// >
|
|
// ▶️ Start Service
|
|
// </button>
|
|
// <button
|
|
// onClick={stopService}
|
|
// disabled={!serviceRunning}
|
|
// style={{
|
|
// flex: 1,
|
|
// padding: '12px 24px',
|
|
// background: !serviceRunning ? '#e5e7eb' : 'linear-gradient(135deg, #ef4444, #dc2626)',
|
|
// color: !serviceRunning ? '#9ca3af' : 'white',
|
|
// border: 'none',
|
|
// borderRadius: '8px',
|
|
// fontSize: '15px',
|
|
// fontWeight: '500',
|
|
// cursor: !serviceRunning ? 'not-allowed' : 'pointer',
|
|
// transition: 'all 0.2s'
|
|
// }}
|
|
// >
|
|
// ⏹️ Stop Service
|
|
// </button>
|
|
// </div>
|
|
// <div style={{ fontSize: '14px' }}>
|
|
// <span style={{ color: serviceRunning ? '#10b981' : '#6b7280', fontWeight: '500' }}>
|
|
// {serviceRunning ? '● Service Running' : '○ Service Stopped'}
|
|
// </span>
|
|
// {connectionAttempts.current > 0 && !connected && (
|
|
// <span style={{ color: '#f59e0b', marginLeft: '12px' }}>
|
|
// 🔄 Reconnecting... (attempt {connectionAttempts.current})
|
|
// </span>
|
|
// )}
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div style={{
|
|
// background: 'white',
|
|
// borderRadius: '12px',
|
|
// padding: '32px',
|
|
// marginBottom: '20px',
|
|
// boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
|
// minHeight: '200px',
|
|
// display: 'flex',
|
|
// alignItems: 'center',
|
|
// justifyContent: 'center'
|
|
// }}>
|
|
// {weight ? (
|
|
// <div style={{ textAlign: 'center', width: '100%' }}>
|
|
// <div style={{ marginBottom: '16px' }}>
|
|
// <span style={{ fontSize: '64px', fontWeight: '700', color: '#1f2937' }}>
|
|
// {weight.weight.toFixed(2)}
|
|
// </span>
|
|
// <span style={{ fontSize: '32px', fontWeight: '600', color: '#6b7280', marginLeft: '8px' }}>
|
|
// {weight.unit}
|
|
// </span>
|
|
// </div>
|
|
// <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '16px' }}>
|
|
// <div style={{
|
|
// padding: '6px 12px',
|
|
// borderRadius: '6px',
|
|
// background: weight.stable ? '#d1fae5' : '#fef3c7',
|
|
// color: weight.stable ? '#065f46' : '#92400e',
|
|
// fontSize: '13px',
|
|
// fontWeight: '500'
|
|
// }}>
|
|
// {weight.stable ? '✓ Stable' : '⋯ Measuring'}
|
|
// </div>
|
|
// <span style={{ color: '#9ca3af', fontSize: '13px' }}>
|
|
// {formatTime(weight.timestamp)}
|
|
// </span>
|
|
// </div>
|
|
// </div>
|
|
// ) : (
|
|
// <div style={{ textAlign: 'center', color: '#9ca3af' }}>
|
|
// <div style={{ fontSize: '64px', marginBottom: '16px' }}>⚖️</div>
|
|
// <p style={{ margin: 0, fontSize: '16px' }}>
|
|
// {connected
|
|
// ? 'Place item on scale...'
|
|
// : serviceRunning
|
|
// ? 'Connecting...'
|
|
// : 'Click "Start Service" to begin'}
|
|
// </p>
|
|
// </div>
|
|
// )}
|
|
// </div>
|
|
|
|
// {history.length > 0 && (
|
|
// <div style={{
|
|
// background: 'white',
|
|
// borderRadius: '12px',
|
|
// padding: '24px',
|
|
// marginBottom: '20px',
|
|
// boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
|
// }}>
|
|
// <h2 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600' }}>Recent Readings</h2>
|
|
// <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
// {history.map((reading) => (
|
|
// <div
|
|
// key={reading.timestamp}
|
|
// style={{
|
|
// display: 'flex',
|
|
// justifyContent: 'space-between',
|
|
// alignItems: 'center',
|
|
// padding: '12px',
|
|
// background: '#f9fafb',
|
|
// borderRadius: '8px',
|
|
// fontSize: '14px'
|
|
// }}
|
|
// >
|
|
// <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
// <span style={{ fontWeight: '600', color: '#1f2937' }}>
|
|
// {reading.weight.toFixed(2)} {reading.unit}
|
|
// </span>
|
|
// {reading.stable && (
|
|
// <span style={{
|
|
// padding: '2px 8px',
|
|
// borderRadius: '4px',
|
|
// background: '#d1fae5',
|
|
// color: '#065f46',
|
|
// fontSize: '12px',
|
|
// fontWeight: '500'
|
|
// }}>
|
|
// Stable
|
|
// </span>
|
|
// )}
|
|
// </div>
|
|
// <span style={{ color: '#9ca3af', fontSize: '13px' }}>
|
|
// {formatTime(reading.timestamp)}
|
|
// </span>
|
|
// </div>
|
|
// ))}
|
|
// </div>
|
|
// </div>
|
|
// )}
|
|
|
|
// {!connected && !serviceRunning && (
|
|
// <div style={{
|
|
// background: '#fef3c7',
|
|
// borderRadius: '12px',
|
|
// padding: '24px',
|
|
// border: '1px solid #fcd34d'
|
|
// }}>
|
|
// <h3 style={{ margin: '0 0 12px 0', fontSize: '16px', fontWeight: '600', color: '#92400e' }}>
|
|
// 🎯 Service Not Running
|
|
// </h3>
|
|
// <p style={{ margin: '0 0 12px 0', color: '#78350f', fontSize: '14px' }}>
|
|
// Click "Start Service" to connect to the weight scale
|
|
// </p>
|
|
// <ul style={{ margin: 0, paddingLeft: '20px', color: '#78350f', fontSize: '13px' }}>
|
|
// <li>Service will start automatically</li>
|
|
// <li>USB scale should be connected to Android device</li>
|
|
// <li>WebSocket server on port 8080</li>
|
|
// </ul>
|
|
// </div>
|
|
// )}
|
|
// </div>
|
|
);
|
|
};
|
|
|
|
export default WeightScaleApp; |