Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,7 @@ function App() {
onFormatChange={setSendFormat}
onSend={handleSendData}
isConnected={connectionStatus.is_connected}
config={config}
checksumConfig={checksumConfig}
onChecksumConfigChange={setChecksumConfig}
quickCommandLists={quickCommandLists}
Expand Down
41 changes: 39 additions & 2 deletions frontend/src/components/SendPanel.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -71,6 +72,7 @@ const SendPanel: React.FC<SendPanelProps> = ({
onFormatChange,
onSend,
isConnected,
config,
checksumConfig,
onChecksumConfigChange,
quickCommandLists,
Expand All @@ -92,6 +94,29 @@ const SendPanel: React.FC<SendPanelProps> = ({
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)
Expand Down Expand Up @@ -577,6 +602,18 @@ const SendPanel: React.FC<SendPanelProps> = ({
</div>
</div>

{/* Rate guard: scheduled sending faster than the line can carry */}
{showRateWarning && (
<div className="mt-2 flex items-center space-x-1.5 flex-shrink-0">
<AlertTriangle size={13} style={{ color: '#f59e0b' }} className="flex-shrink-0" />
<span className="text-xs" style={{ color: '#f59e0b' }}>
{t('sendPanel.rateExceeded')
.replace('{rate}', formatRate(requiredRateBps))
.replace('{capacity}', formatRate(lineCapacityBps))}
</span>
</div>
)}

{/* Quick Insert Row (Hex mode only) */}
{format === 'Hex' && (
<div
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"stopScheduled": "Stop scheduled sending",
"startScheduled": "Start scheduled sending",
"intervalTitle": "Send interval in milliseconds",
"rateExceeded": "Send rate ~{rate} exceeds line capacity {capacity} — sends will keep timing out",
"characters": "Characters",
"bytes": "Bytes",
"quickInsert": "Quick Insert",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"stopScheduled": "停止定时发送",
"startScheduled": "开始定时发送",
"intervalTitle": "发送间隔(毫秒)",
"rateExceeded": "发送速率约 {rate},超过线路容量 {capacity},将持续超时",
"characters": "字符数",
"bytes": "字节数",
"quickInsert": "快速插入",
Expand Down
20 changes: 19 additions & 1 deletion src-tauri/src/serial_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ impl PortOpener for SystemPortOpener {
}
}

/// Write-side timeout (POSIX only, see `connect`). Much longer than the
/// 50 ms read timeout so large or bursty payloads tolerate a full kernel TX
/// buffer draining at line rate instead of failing spuriously.
#[cfg(unix)]
const WRITE_TIMEOUT_MS: u64 = 1000;

impl SerialManager {
pub fn new() -> Self {
// Default log directory - will be overridden by frontend settings
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading