123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- 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<string> RWLog;
- public event Action<bool> SerialConnected; //串口是否已连接
- public event Action<bool> SerialStatusChanged; //串口连接状态变化
- public event Action<ushort,ushort> 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<byte> dataByteList = new List<byte>();
- 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);
- }
- }
- //----------------------------------------------------------
- }
- }
|