From edbdc39f43bb08ab4f2a62ee1fc0d8a811f62916 Mon Sep 17 00:00:00 2001 From: gyanano <1624055384@qq.com> Date: Wed, 2 Sep 2026 02:25:52 -0700 Subject: [PATCH] feat(serial): longer POSIX write timeout + scheduled-send rate guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposed by hardware testing after RFC #3 Step 2: at 115200 8N1 a 2560 B / 100 ms scheduled send (25.6 KB/s) overruns the 11.52 KB/s line capacity and fails with 'Operation timed out' once the kernel TX buffer fills. - Backend: give the write handle a 1000 ms timeout on POSIX (timeout lives per-handle there, so the 50 ms read loop is unaffected). Windows shares COMMTIMEOUTS across cloned handles, so the write side keeps the builder value there. Per-poll timeout semantics mean borderline rates with bursts now drain instead of failing spuriously. - Frontend: SendPanel warns when a scheduled send's required rate exceeds 90% of the line capacity derived from baud/data/parity/stop bits, so physically impossible rates are flagged before the user hits timeouts. Remaining TX-queue work (write-behind thread, backpressure, failure reporting) tracked in #7 — needs RFC #3 Step 4 events as its channel. --- frontend/src/App.tsx | 1 + frontend/src/components/SendPanel.tsx | 41 +++++++++++++++++++++-- frontend/src/i18n/translations/en.json | 1 + frontend/src/i18n/translations/zh-CN.json | 1 + src-tauri/src/serial_manager.rs | 20 ++++++++++- 5 files changed, 61 insertions(+), 3 deletions(-) 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()