C# SerialPort Неполный прием сообщений
От: ethen  
Дата: 22.05.13 16:23
Оценка:
Добрый день. Долго искал решение своей проблемы, но поиски оказались тщетны.
Задача: организовать обмен сообщениями между железом и компом, посредством com порта на С# при использовании определенного протокола.
Железо основано на PIC24, ПО работает как часы.
Проблема: примерно в 95% случаях моя программа считывает не всё пришедшее сообщение, а только часть.
Вот пример, лог программы Portmon
    
1072    0.00000419    Com_1.exe    IOCTL_SERIAL_GET_COMMSTATUS    VCP0    SUCCESS        
1073    0.00001145    Com_1.exe    IRP_MJ_READ                    VCP0    SUCCESS    Length 10: FF FF 2E A1 B1 03 44 35 01 09     
1074    0.91130577    Com_1.exe    IOCTL_SERIAL_WAIT_ON_MASK      VCP0    SUCCESS                
1079    0.00000447    Com_1.exe    IOCTL_SERIAL_GET_COMMSTATUS    VCP0    SUCCESS        
1080    0.00001117    Com_1.exe    IRP_MJ_READ                    VCP0    SUCCESS    Length 3: FF FF 2E     
1081    0.01545727    Com_1.exe    IOCTL_SERIAL_WAIT_ON_MASK      VCP0    SUCCESS        
1082    16.02207878   Com_1.exe    IOCTL_SERIAL_WAIT_ON_MASK      VCP0    SUCCESS            
1084    0.00000419    Com_1.exe    IOCTL_SERIAL_GET_COMMSTATUS    VCP0    SUCCESS        
1085    0.00001090    Com_1.exe    IRP_MJ_READ                    VCP0    SUCCESS    Length 7: A1 B1 03 44 36 01 C3


Первое сообщение принято сразу полностью. И я в программе его нормально обработал. Второе принято двумя кусками, соответственно я этот пакет просто отбросил как не соответствующий протоколу.

Далее код. Привожу не весь чтобы не загромождать сообщение.

Открытие порта.
public SerialPort sp = new SerialPort();
public bool Open(string portName, int baudRate, int databits, Parity parity, StopBits stopBits)
        {
            if (!sp.IsOpen)
            {
                sp.PortName = portName;
                sp.BaudRate = baudRate;
                sp.DataBits = databits;
                sp.Parity = parity;
                sp.StopBits = stopBits;
                sp.ReadTimeout = 1000;
                sp.WriteTimeout = 1000;
                
                sp.DtrEnable = false;
                sp.RtsEnable = false;
                sp.ReadBufferSize = 4096;
                sp.WriteBufferSize = 4096;
                
                try
                {
                    sp.Open();
                }
                catch (Exception err)
                {
                    comptStatus = "Error opening " + portName + ": " + err.Message;
                    return false;
                }
                Status = portName + " opened successfully";
                sp.DataReceived += new SerialDataReceivedEventHandler(Port_DataReceived);

                return true;
            }
            else
            {
                Status = portName + " already opened";
                return false;
            }
        }

Функция вызываемая по событию прихода данных в порт

void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            // способ 1, прием всего пакета по sp.BytesToRead;
            byte[] bufer = recieve_data();
            // и дальше  обработка....
        }

Собственно функция приема сообщения из порта
public byte[] recieve_data()
        {
            try
            {               
                byte[] bytes = new byte[sp.BytesToRead];
                sp.Read(bytes, 0, bytes.Length);
                Status = "Read successful";
                return bytes;
            }
            catch (Exception err)
            {
                comptStatus = "Error in read event: " + err.Message;
                return null;
            }
        }

Собственно на этом и возникает описанная в начале поста проблема.
Первой попыткой решить было — принимать данные не по значению поля sp.BytesToRead а по своему протоколу.
алгоритм такой же. жду события прихода какой то информации в порт. Если это старт байт, дочитываю жестко шапку пакета, оттуда читаю сколько еще надо дочитать. код такой же что и выше , просто sp.Read() вызывается не 1 раз а 3.
В результате я лишь увеличил вероятность ошибочного приема до 8% т.к. вместо одинарного считывания на 1 пакет в 10 байт, я произвожу целых 3 по 1, 4, 5 байт друг за другом. и в последних двух считываниях и возникает та же проблема не полного прочтения пакета.
Следующей попыткой был "циклический буфер". не знаю на сколько я корректно его реализовал:
        public byte[] roundRecieveData()
        {
            try 
            {
                List<byte> bytelist = new List<byte>();
                int readLen = 0;
                int _len = 0;
                do
                {
                    _len = sp.BytesToRead;

                    if (_len < 10 )
                    {;}
                    
                    byte[] _byte = new byte[_len];
                    readLen = sp.Read(_byte, 0, _len);
                    
                    for (int i = 0; i < _len ; i++)
                        bytelist.Add(_byte[i]);

                    System.Threading.Thread.Sleep(1);
                }while((sp.BytesToRead > 0));
                
                byte[] _bytes = new byte[bytelist.Count];

                for (int i = 0; i < bytelist.Count ; i++)
                        _bytes[i] = bytelist[i];

                return _bytes;
            } 
            catch (System.Exception ex)
            {
                comptStatus = "Error in read event: " + ex.Message;
                return null;
            }
        }

По моей задумке,- пришел неполный пакет. sp.BytesToRead = 4, при первом проходе цикла я читаю 4 байта, и значение sp.BytesToRead меняется на 6 и по циклу дочитывается остальной кусок. На практике выполняется только 1 проход цикла. Do while на всякий случай менял местами — не помогло.
Однако если поставить точку останова на удивительной конструкции "if (_len < 10 ) {;}" и сразу же нажать продолжить. То бишь у нас не полный пакет. то дочитка пакета происходит так как я думал в теории.

последней попыткой стал отказ от события приема байт порта и слежение за sp.BytesToRead из потока, главная функция потока
        private void readThreadFn()
        {
            Byte[] inputBuffer;
            while (true)
            {
                try
                {
                    int availibleBytes = sp.BytesToRead;
                    while (availibleBytes > 0)
                    {
                        inputBuffer = new Byte[availibleBytes];                        
                        int readedBytes = sp.Read(inputBuffer, 0, inputBuffer.Length);
                        //inputBuffer = roundRecieveData(); // и так пробовал - не помогло
                        OnReceiveByte(inputBuffer); // вывоз события обработки данных
                    }
                    System.Threading.Thread.Sleep(1);
                }
                catch (System.Threading.ThreadAbortException)
                {  break; }
                catch (Exception e)
                {
                    System.Diagnostics.Debug.WriteLine(e.Message);
                }
            }
        }

Увы проблема осталась. Так же как и в простом считывании сообщения из порта в 5% случая оно читается не все сразу.

Подскажите пожалуйста как решить данную проблему
c# serialport
Re: C# SerialPort Неполный прием сообщений
От: Neco  
Дата: 22.05.13 17:17
Оценка: 3 (1) +2
Здравствуйте, ethen, Вы писали:

E>принято двумя кусками

это нормально. при работе с потоками данных будьте готовы, что данные будут приходить частями или по полтора-два-десять склеенных "сообщений".
переписывайте алгоритм. должно быть что-то вроде:
1. появились данные на порту — читаем в буфер, пока не выполнится условие-признак окончания сообщения.
2. условием-признаком может быть — фиксированная длина сообщения, спец.байт (символ переноса строки, например) или количество байт должно идти первым делом (первые четыре байта — длина оставшейся части сообщения).
3. откусываем полные сообщения, передаём на обработку (парсинг и т.д.). остаток запоминаем и к нему приплюсовываем последующие куски сообщений.

если команды разделяются переносом строки, то можно юзать ReadLine метод — работал вроде без сбоев.
всю ночь не ем, весь день не сплю — устаю
Re[2]: C# SerialPort Неполный прием сообщений
От: Neco  
Дата: 22.05.13 19:42
Оценка: 3 (1)
пример

    class Program {
        static void Main(string[] args) {
            TestVaryLength(new string[] { "123456789" }, new int[] { 4, 9 });
            TestVaryLength(new string[] { "123456789" }, new int[] { 4, 4, 4, 1 });
            TestVaryLength(new string[] { "123456789" }, new int[] { 3, 5, 4, 1 });

            TestVaryLength(new string[] { "123456789", "123" }, new int[] { 3, 5, 4, 8 });
            TestVaryLength(new string[] { "123456789", "123" }, new int[] { 3, 5, 4, 1, 5, 2 });

            TestVaryLength(new string[] { "123456789", "123", "12345" }, new int[] { 3, 5, 4, 17 });
            TestVaryLength(new string[] { "123456789", "123", "12345" }, new int[] { 3, 5, 4, 1, 16 });
            TestVaryLength(new string[] { "123456789", "123", "12345" }, new int[] { 3 + 5 + 4 + 1 + 16 });
            TestVaryLength(new string[] { "123456789", "123", "12345" }, new int[] { 3 + 5 + 4 + 1 + 15, 1 });
        }

        private static void TestVaryLength(string[] textArray, int[] partsSize) {
            SerialPortEmulator serialPort = new SerialPortEmulator();

            IMessageReader msgRdr = new MessageReaderVaryLength(serialPort);

            Queue<string> expected = new Queue<string>(textArray);
            msgRdr.MessageArrived += (sender, e) => {
                var expText = expected.Dequeue();
                var actText = BytesToString(e.Data);
                Test(expText, actText);
            };
            List<byte> bufferPrep = new List<byte>();
            foreach (var oneText in textArray) {
                var buf = StringToBytes(oneText);
                bufferPrep.AddRange(BitConverter.GetBytes(buf.Length));
                bufferPrep.AddRange(buf);
            }
            var buffer = bufferPrep.ToArray();
            int written = 0;
            foreach (var onePartSize in partsSize) {
                serialPort.PutData(TakePart(buffer, written, onePartSize));
                serialPort.RaiseDataReceived();
                written += onePartSize;
            }
            if (written != buffer.Length)
                throw new InvalidOperationException(string.Format("The test is not conformed well. Written [{0}] bytes, in buffer [{1}] bytes.", written, buffer.Length));

            if (expected.Count > 0)
                throw new InvalidOperationException("Test failed - not all elements were read.");

            Console.WriteLine("Ok");
        }
        private static string BytesToString(byte[] array) {
            return Encoding.UTF8.GetString(array);
        }
        private static byte[] StringToBytes(string text) {
            return Encoding.UTF8.GetBytes(text);
        }
        private static byte[] TakePart(byte[] buffer, int offset, int count) {
            var rez = new byte[count];
            Array.Copy(buffer, offset, rez, 0, count);
            return rez;
        }

        private static void Test(byte[] expected, byte[] actual) {
            if (!Array.Equals(actual, expected)) {
                throw new InvalidOperationException("Arrays are not equal.");
            }
        }
        private static void Test(string expected, string actual) {
            if (string.Compare(actual, expected,  StringComparison.Ordinal) != 0) {
                throw new InvalidOperationException(string.Format("Expected [{0}], actual [{1}].", expected, actual));
            }
        }
    
    }
    interface ISerialPort {
        event EventHandler DataReceived;
        int Read(byte[] buffer, int offset, int count);
        int ReadInt32();
        int BytesToRead { get; }
    }
    class SerialPortEmulator : ISerialPort {
        private List<byte> _allData = new List<byte>();
        public event EventHandler DataReceived;

        public int Read(byte[] buffer, int offset, int count) {
            int allDataLength = _allData.Count;
            int factCount = Math.Min(allDataLength, count);

            _allData.CopyTo(0, buffer, offset, factCount);

            _allData.RemoveRange(0, factCount);

            return factCount;
        }
        public int ReadInt32() {
            int intSize = sizeof(int);
            if (_allData.Count < intSize)
                throw new InvalidOperationException(string.Format("Cannot read int from the array which contains [{0}] bytes.", _allData.Count));

            byte[] tmpBuf = new byte[intSize];
            Read(tmpBuf, 0, intSize);
            return BitConverter.ToInt32(tmpBuf, 0);
        }

        public void PutData(byte[] data) {
            _allData.AddRange(data);
        }
        public void RaiseDataReceived() {
            var handler = DataReceived;
            if (handler != null) {
                handler(this, EventArgs.Empty);
            }
        }
        public int BytesToRead {
            get { return _allData.Count; }
        }
    }
    interface IMessageReader {
        event EventHandler<MessageArrivedEventArgs> MessageArrived;
    }
    class MessageArrivedEventArgs : EventArgs {
        public MessageArrivedEventArgs(byte[] data) {
            Data = data;
        }
        public byte[] Data { get; private set; }
    }
    class MessageReaderVaryLength : IMessageReader {
        private ISerialPort _serialPort;
        private const int SMALL_BUF_SIZE = 5;
        private byte[] _buffer;
        private int _bytesHave;
        private int _expectedSize;
        private State _state;

        public event EventHandler<MessageArrivedEventArgs> MessageArrived;

        public MessageReaderVaryLength(ISerialPort serialPort) {
            _state = State.ReadingSize;
            _serialPort = serialPort;
            _serialPort.DataReceived += new EventHandler(_serialPort_DataReceived);
        }

        void _serialPort_DataReceived(object sender, EventArgs e) {
            int bytesToRead = _serialPort.BytesToRead;
            bool exitLoop = false;
            while (!exitLoop) {
                switch (_state) {
                    case State.ReadingSize: {
                            if (bytesToRead >= 4) {
                                _expectedSize = _serialPort.ReadInt32();
                                _buffer = new byte[_expectedSize];
                                _bytesHave = 0;
                                bytesToRead -= 4;
                                _state = State.ReadingMessage;
                            } else {
                                exitLoop = true;
                            }
                            break;
                        }
                    case State.ReadingMessage: {
                            int sizeToRead = _expectedSize - _bytesHave;
                            int factRead = _serialPort.Read(_buffer, _bytesHave, sizeToRead);
                            _bytesHave += factRead;
                        
                            if (_bytesHave > _expectedSize)
                                throw new InvalidOperationException("Something went wrong - have read more than expected.");

                            if (_bytesHave == _expectedSize) {
                                OnMessageArrived(_buffer);
                                _state = State.ReadingSize;
                            }

                            bytesToRead -= factRead;
                            break;
                        }
                    default: throw new InvalidOperationException(string.Format("Unknown state [{0}]", _state));
                }
                
                if (bytesToRead < 0)
                    throw new InvalidOperationException("Something went wrong - 'bytes to read' is less than zero.");

                if (bytesToRead == 0)
                    exitLoop = true;
            }
        }
        private void OnMessageArrived(byte[] buffer) {
            var handler = MessageArrived;
            if (handler != null) {
                handler(this, new MessageArrivedEventArgs(buffer));
            }
        }

        private enum State {
            ReadingSize, ReadingMessage
        }

    }
всю ночь не ем, весь день не сплю — устаю
Re[2]: C# SerialPort Неполный прием сообщений
От: andrey82  
Дата: 23.05.13 10:37
Оценка: 3 (1)
Здравствуйте, Neco, Вы писали:

N>это нормально. при работе с потоками данных будьте готовы, что данные будут приходить частями или по полтора-два-десять склеенных "сообщений".

N>переписывайте алгоритм. должно быть что-то вроде:
N>...

Добавлю еще:
1) первый поток читает из порта порциями данные, кладет в буфер.
2) второй поток ищет в буфере целые сообщения, выбирает их и обрабатывает сам или куда-то передает.
Естественно, не забываем о синхронизации потоков.
3) дополнительно для повышения надежности (на случай если линия связи с помехами или программа может начать читать порт в произвольный момент, с середины передаваемого сообщения): отслеживаем остающиеся в буфере куски, чтобы они не накапливались; разбираем сообщение, используя конечный автомат (по возможности).
Re[3]: C# SerialPort Неполный прием сообщений
От: ethen  
Дата: 25.05.13 12:25
Оценка:
Здравствуйте, Neco, Вы писали:

N>пример


Спасибо большое за помощь.
Я если честно ожидал более простого решения где то я находил на просторах интернета пример на winapi, прием данных сделан просто через Read() и обработку возникающих ошибок порта.
Но раз его нет, буду разбираться с вашим примером.
Re: C# SerialPort Неполный прием сообщений
От: Sinatr Германия  
Дата: 27.05.13 08:12
Оценка:
Здравствуйте, ethen, Вы писали:

E>Проблема: примерно в 95% случаях моя программа считывает не всё пришедшее сообщение, а только часть.


Это нормально, т.к. событие DataReceived не обязательно срабатывает для каждого полученного байта.

Решение очень простое — вызывать DataReceived пока не закончаться байты:

        private static void DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            // prevent error with closed port to appears
            if (!_port.IsOpen)
                return;
            // reading
            if (_port.BytesToRead > 0)
            {

                // reading here _port.ReadByte()
                // processing here

                // call again
                DataReceived(sender, e);
            }
        }


Еще причиной может быть таймаут на чтение.
---
ПроГLамеры объединяйтесь..
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.