using System; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MV485.helper { //发送方: //等待接收方发送 ‘C’(表示使用CRC模式)或 NAK(使用Checksum)。 //发送文件数据,每次128字节,并计算 Checksum 或 CRC。 //发送 EOT(表示传输完成)。 //接收方: //发送 ‘C’ 或 NAK 以请求数据。 //解析数据包并校验。 //发送 ACK 确认正确接收。 //每一帧的结构如下,共 132 字节: //字节位置 名称 值 / 范围 描述 //1 SOH 0x01 起始标志,表示是 128 字节数据包 //2 Block Number 0x01 ~ 0xFF 当前包编号(从1开始) //3 Block Number ^ 255 - Block Number 包编号的反码(用于校验) //4-131 Data[128] 任意数据 实际数据,末尾不足用 0x1A 填充 //132 Checksum 所有数据字节的累加和 校验和:Data[0] + ... + Data[127](8-bit) public class XModemReceiver { private SerialPort _serialPort; private string _portName; private const int FRAME_MAX_MILLISECONDS = 2000; //每一帧最大超时时间 private const int PACKET_SIZE = 128; private const byte SOH = 0x01; //128字节的头标志 private const byte EOT = 0x04; //发送结束的标志 private const byte ACK = 0x06; private const byte NAK = 0x15; // private const byte CAN = 0x18; private const byte C = 0x43; private string _message; public event Action RWLog; public event Action SerialConnected; //串口是否已连接 public event Action SerialStatusChanged; //串口连接状态变化 public event Action OnProgress; private bool _isReceiving = false; public XModemReceiver() { } //开始接收文件 public bool StartReceiveFile(string portName,int baudRate,string savePath,ushort fileSize) { bool blResult = false; _portName = portName; if (OpenSerial(portName, baudRate)) { _isReceiving = true; blResult = ReceiveFile(savePath,fileSize); } CloseSerial(); return blResult; } private bool ReceiveFile(string savePath,ushort fileSize) { bool blResult = false; List dataByteList = new List(); int blockNumber = 1; try { _serialPort.DiscardInBuffer(); //发送NAK请求数据(使用校验和) _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); Thread.Sleep(100); while (_isReceiving) { //Thread.Sleep(10); //---开始接收一帧数据--- DateTime frameStartTime = DateTime.Now; bool frameReceived = false; // 在内部循环中尝试接收完整一帧,2秒内必须完成 while (!frameReceived) { //Thread.Sleep(100); if ((DateTime.Now - frameStartTime).TotalMilliseconds > FRAME_MAX_MILLISECONDS) { //int n = _serialPort.Read(data, read, PACKET_SIZE - read); byte[] tempData = new byte[PACKET_SIZE]; int n = _serialPort.Read(tempData, 0, PACKET_SIZE); if (n > 0) { _message = BitConverter.ToString(tempData, 0, n).Replace("-", " "); RWLog?.Invoke($"读取到: {_message}"); } RWLog?.Invoke("接收当前帧超时(超过2秒),终止文件接收。"); return false; } //10ms内读取串口中的第一个字节 int b = ReadByte(100); if (b == -1) { // 未读取到数据,发送 NAK 重试 _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); continue; } //接收到EOT,表示文件传输完成 if (b == EOT) { _serialPort.Write(new byte[] { ACK }, 0, 1); RWLog?.Invoke($"发送ACK: {ACK:X2}"); RWLog?.Invoke("文件传输完成"); blResult = true; frameReceived = true; // 退出当前帧循环 _isReceiving = false; break; } //头标志不是SOH(128字节的xModem的头标志) if (b != SOH) { RWLog?.Invoke($"收到数据帧头标志错误: 0x{b:X2}"); _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); continue; } // 读取包号与包号补码 int blockNum = ReadByte(100); int blockNumComp = ReadByte(100); if ((blockNum + blockNumComp) != 0xFF) { _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); RWLog?.Invoke("包号补码错误"); continue; } // 检查是否为重复帧(重复帧直接 ACK 后跳过) if (blockNum == ((blockNumber - 1) & 0xFF)) { RWLog?.Invoke($"重复数据包 #{blockNum},重新发送 ACK"); _serialPort.Write(new byte[] { ACK }, 0, 1); RWLog?.Invoke($"发送ACK: {ACK:X2}"); frameReceived = true; break; } // 读取 128 字节数据,整个读取也必须在2秒内完成 if (blockNum != (blockNumber & 0xFF)) { RWLog?.Invoke("包号不符合预期"); _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); continue; } //读取128字节的数据 byte[] data = new byte[PACKET_SIZE]; int read = 0; while (read < PACKET_SIZE && (DateTime.Now - frameStartTime).TotalMilliseconds < 2000) { if (_serialPort.BytesToRead > 0) { int n = _serialPort.Read(data, read, PACKET_SIZE - read); read += n; } else { Thread.Sleep(10); } } if (read < PACKET_SIZE) { RWLog?.Invoke("数据包接收超时或不完整"); _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); continue; } //读取校验和 int checksum = ReadByte(100); int sum = 0; foreach (byte bt in data) { sum = (sum + bt) % 256; } //校验和不通过 if (checksum != sum) { RWLog?.Invoke($"校验和验证不通过"); _serialPort.Write(new byte[] { NAK }, 0, 1); RWLog?.Invoke($"发送NAK: {NAK:X2}"); continue; } // 当前帧数据接收成功 dataByteList.AddRange(data); //RWLog?.Invoke($"第{blockNumber}包"); blockNumber++; frameReceived = true; _serialPort.Write(new byte[] { ACK }, 0, 1); //RWLog?.Invoke($"发送ACK: {ACK:X2}"); }// end of while(!frameReceived) //double progress = ((double)dataByteList.Count) / fileSize; //progress = progress > 1 ? 1.0 : progress; OnProgress?.Invoke((ushort)dataByteList.Count,fileSize); // 如果文件数据已经足够,则退出整体接收循环 //if (dataByteList.Count >= fileSize || blResult) // break; }// end of while(_isReceiving) //保存文件 //if (blResult && dataByteList.Count >= fileSize) if (blResult || dataByteList.Count >= fileSize) { //移除填充的0x1A,因为已经知道文件大小了 byte[] fileBytes = new byte[fileSize]; Array.Copy(dataByteList.ToArray(), fileBytes, fileSize); string directory = Path.GetDirectoryName(savePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } File.WriteAllBytes(savePath, fileBytes); RWLog?.Invoke($"已保存文件{savePath}"); } else { RWLog?.Invoke($"未完整接收采样照片"); } } catch(IOException ex) { _message = $"接收文件IO错误:{ex.Message}"; RWLog?.Invoke(_message); blResult = false; } catch(Exception ex) { _message = $"接收文件错误:{ex.Message}"; RWLog?.Invoke(_message); blResult = false; } return blResult; } public void StopReceiveFile() { _isReceiving = false; if (_serialPort?.IsOpen == true) { _serialPort.Write(new byte[] { CAN, CAN }, 0, 2); Thread.Sleep(100); } } private int ReadByte(int timeoutMilliseconds) { DateTime deadline = DateTime.Now.AddMilliseconds(timeoutMilliseconds); while (DateTime.Now < deadline) { if (_serialPort.BytesToRead > 0) return _serialPort.ReadByte(); Thread.Sleep(10); // 睡眠 10 毫秒,避免过于频繁地轮询 } return -1; // 超时未读取到数据 } private byte[] TrimTrailingPadding(byte[] data, int maxLength) { if (data.Length > maxLength) Array.Resize(ref data, maxLength); return data; } private bool OpenSerial(string portName, int baudRate) { CloseSerial(); try { //除去波特率外,其它参数默认为不会被修改 SerialPort serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); serialPort.ReadTimeout = 1000; //读取的超时时间应该短一些 serialPort.WriteTimeout = 1000; serialPort.Open(); _serialPort = serialPort; _message = $"串口{portName}连接成功"; RWLog?.Invoke(_message); Logger.Info(_message); SerialConnected?.Invoke(true); return true; } catch (Exception ex) { _message = $"串口{portName}连接失败"; RWLog?.Invoke(_message); SerialConnected?.Invoke(false); Logger.Info(_message); Logger.Error(ex.Message); return false; } } private void CloseSerial() { try { if (_serialPort != null) { if (_serialPort.IsOpen) { _serialPort.Close(); _message = $"串口{_portName}已关闭"; SerialStatusChanged?.Invoke(false); //串口关闭 RWLog?.Invoke(_message); Logger.Info(_message); } _serialPort.Dispose(); _serialPort = null; } } catch (Exception ex) { _message = $"关闭串口{_portName}失败: {ex.Message}"; RWLog?.Invoke(_message); Logger.Error(_message); } } //---------------------------------------------------------- } }