diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ccbe142..0730314 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -855,6 +855,7 @@ function App() { onFormatChange={setSendFormat} onSend={handleSendData} isConnected={connectionStatus.is_connected} + config={config} checksumConfig={checksumConfig} onChecksumConfigChange={setChecksumConfig} quickCommandLists={quickCommandLists} diff --git a/frontend/src/components/SendPanel.tsx b/frontend/src/components/SendPanel.tsx index daee370..841b65d 100644 --- a/frontend/src/components/SendPanel.tsx +++ b/frontend/src/components/SendPanel.tsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; -import { Send, Shield, ChevronDown, ChevronUp, List, FileText } from 'lucide-react'; -import { DataFormat, ChecksumType, ChecksumConfig, QuickCommandList, QuickCommand, LineEnding } from '../types'; +import { Send, Shield, ChevronDown, ChevronUp, List, FileText, AlertTriangle } from 'lucide-react'; +import { DataFormat, ChecksumType, ChecksumConfig, QuickCommandList, QuickCommand, LineEnding, SerialConfig } from '../types'; import QuickCommandPanel from './QuickCommandPanel'; import { useTheme } from '../contexts/ThemeContext'; import { useTranslation } from '../i18n'; @@ -51,6 +51,7 @@ interface SendPanelProps { onFormatChange: (format: DataFormat) => void; onSend: () => void; isConnected: boolean; + config: SerialConfig; checksumConfig: ChecksumConfig; onChecksumConfigChange: (config: ChecksumConfig) => void; // Quick Command props @@ -71,6 +72,7 @@ const SendPanel: React.FC = ({ onFormatChange, onSend, isConnected, + config, checksumConfig, onChecksumConfigChange, quickCommandLists, @@ -92,6 +94,29 @@ const SendPanel: React.FC = ({ const [isChecksumExpanded, setIsChecksumExpanded] = useState(false); const [isConverting, setIsConverting] = useState(false); + // Line capacity in bytes/s: 1 start bit + data bits + optional parity bit + // + stop bits per byte on the wire. + const lineCapacityBps = useMemo(() => { + const dataBits = { Five: 5, Six: 6, Seven: 7, Eight: 8 }[config.data_bits]; + const parityBit = config.parity === 'None' ? 0 : 1; + const stopBits = { One: 1, OnePointFive: 1.5, Two: 2 }[config.stop_bits]; + return config.baud_rate / (1 + dataBits + parityBit + stopBits); + }, [config]); + + // Payload per scheduled send in bytes (UTF-8 approximation in Text mode; + // this is a guard rail, not an exact meter). + const payloadBytes = useMemo(() => { + const base = format === 'Hex' + ? value.replace(/\s/g, '').length / 2 + : new TextEncoder().encode(value).length; + return base + getChecksumLength(checksumConfig.type); + }, [value, format, checksumConfig.type]); + + const requiredRateBps = (payloadBytes * 1000) / scheduledInterval; + const showRateWarning = isScheduledEnabled && payloadBytes > 0 && requiredRateBps > lineCapacityBps * 0.9; + const formatRate = (bps: number) => + bps >= 1024 ? `${(bps / 1024).toFixed(1)} KB/s` : `${Math.round(bps)} B/s`; + // Calculate disabled state for normal mode (depends on both connection AND content) const isNormalSendDisabled = !isConnected || !value.trim() || isScheduledEnabled || isConverting; // Calculate disabled state for quick mode (depends ONLY on connection) @@ -577,6 +602,18 @@ const SendPanel: React.FC = ({ + {/* Rate guard: scheduled sending faster than the line can carry */} + {showRateWarning && ( +
+ + + {t('sendPanel.rateExceeded') + .replace('{rate}', formatRate(requiredRateBps)) + .replace('{capacity}', formatRate(lineCapacityBps))} + +
+ )} + {/* Quick Insert Row (Hex mode only) */} {format === 'Hex' && (
Self { // Default log directory - will be overridden by frontend settings @@ -170,7 +176,10 @@ impl SerialManager { self.disconnect()?; } - let port = self.port_opener.open(port_name, &config)?; + // `mut` is only exercised by the POSIX write-timeout tweak below; + // Windows shares timeouts across cloned handles and leaves it alone. + #[allow(unused_mut)] + let mut port = self.port_opener.open(port_name, &config)?; info!("Successfully opened serial port: {}", port_name); // Reset and start reading thread @@ -187,6 +196,15 @@ impl SerialManager { let shutdown_flag = Arc::clone(&self.shutdown_flag); let mut read_port = port.try_clone()?; + // Give the write side a longer timeout than the read-friendly 50 ms. + // On POSIX the timeout lives on each handle, so this does not slow + // the read loop; on Windows cloned handles share COMMTIMEOUTS, so we + // leave the write side at the builder's value there. + #[cfg(unix)] + if let Err(e) = port.set_timeout(Duration::from_millis(WRITE_TIMEOUT_MS)) { + warn!("Failed to set write timeout on {}: {}", port_name, e); + } + thread::spawn(move || { let mut read_buffer = [0u8; 1024]; let initial_config = frame_segmentation_config.lock()