Форум программистов, компьютерный форум, киберфорум
Наши страницы
Rius
Войти
Регистрация
Восстановить пароль
Рейтинг: 1.00. Голосов: 1.

Чтение с последовательного порта без использования Sleep

Запись от Rius размещена 13.08.2016 в 16:32
Обновил(-а) Rius 30.09.2016 в 17:30
Метки .net, c#, serialport, uart

В продолжение темы Обработка данных с COM порта без использования Sleep

Если возникает необходимость обмена с внешним устройством по UART, в C# для этого самый очевидный способ - применение System.IO.Ports.SerialPort.

Рассматривается случай, когда ответ от устройства приходит после посылки запроса к нему.
Если с отправкой всё более-менее понятно, то приём реализуется по разному.

Чтение SerialPort с исключениями.
  • Опрос свойства BytesToRead в цикле затратен по времени и ресурсам.
  • Событие DataReceived тоже кривое. Может прийти в начале приёма данных, может позже. Работает само по себе. Поэтому для схемы запрос-ответ надо городить огород вокруг него, чтобы удостовериться в завершении приёма всех данных.
  • Read(Byte[], Int32, Int32) уже получше, но таймаут у него задаётся один.
Приём ответа состоит из нескольких фаз.
Ответа надо дождаться, а он может и не прийти. Может прийти через длительное время, но быть относительно коротким. Либо наоборот, прийти быстро, и быть длинным.
Надо дождаться начала ответа, принять все его байты, и сделать вывод о завершении ответа.
Вывод этот можно сделать по факту таймаута. Это время, в течение которого от устройства не пришло ни одного байта, время тишины. Его можно принять, например, равным времени, необходимым для передачи трёх байт на выбранной скорости.
Однако, тут появляется пара проблем:
  • Если таймаут небольшой, то можно не дождаться начала ответа.
  • Если таймаут большой, то время ожидания завершения ответа будет потрачено напрасно.

Для обхода этого можно сделать два таймаута, как показано в приведённом ниже коде.
Первый таймаут - ожидание начала ответа.
Второй таймаут - ожидание завершения, если ответ-таки пошёл.
Это позволит и дождаться начала, и не тратить время понапрасну в конце.

Сначала выставляется первый таймаут и запускается приём одного байта.
Если байт не принят, вылетает TimeoutException, значит ответ не пришёл вообще.
Если байт принят, выставляется второй таймаут и запускается приём оставшихся байт.
Приём производится кусками, из которых заполняется буфер. Если чтение прошло без проблем, буфер дополняется новыми байтами и чтение куска перезапускается. Если вылетел TimeoutException, значит байтов больше нет и приём завершён.

В функцию должен передаваться уже настроенный и открытый SerialPort.
Массив байт для передаваемых байт, массив байт для принимаемых байт, переменная под количество принятых байт.
А также токен отмены, на случай необходимости прерывания приёма (но из состояния ожидания таймаута этот токен не выведет).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/// <summary>
/// Отправка/приём данных через SerialPort.
/// </summary>
/// <param name="connection">SerialPort, используемый для связи.</param>
/// <param name="sendBuffer">Массив отправляемых байт.</param>
/// <param name="waitingForStart">Время ожидания от отправки до начала ответа.</param>
/// <param name="waitForEnd">Время ожидания после завершения ответа.</param>
/// <param name="cancellationToken">Токен отмены.</param>
/// <param name="recvBuffer">Массив буфера для приёма байт.</param>
/// <param name="received">Количество принятых байт.</param>
/// <returns>Получен ли какой-нибудь ответ.</returns>
public bool SendRecv(SerialPort connection, byte[] sendBuffer, int waitingForStart, int waitForEnd, CancellationToken cancellationToken, ref byte[] recvBuffer, out int received)
{
    bool result = false;
    received = -1;
 
    try
    {
        // Порт должен быть открыт.
        if (connection.IsOpen)
        {
            // Сброс буферов.
            connection.DiscardInBuffer();
            connection.DiscardOutBuffer();
 
            if (connection.BytesToRead > 0)
            {
                connection.ReadExisting();
            }
 
            // Установка таймаута начала ответа.
            connection.ReadTimeout = waitingForStart;
 
            // Отправка данных.
            connection.Write(sendBuffer, 0, sendBuffer.Length);
 
            int readed = 0;
 
            try
            {
                // Ожидание первого байта. Если ничего не придёт за waitingForStart мс, то вылетит TimeoutException.
                recvBuffer[readed++] = Convert.ToByte(connection.ReadByte());
 
                // Установка таймаута для определения завершения ответа.
                connection.ReadTimeout = waitForEnd;
 
                try
                {
                    // Временный буфер для приёма частями.
                    byte[] temp = new byte[16];
 
                    do
                    {
                        // Чтение и заполнение буфера.
                        // Если ничего не принято, то вылетит TimeoutException.
                        // Если считано меньше или равно размеру буфера, это число возвращается функцией.
                        int b = connection.Read(temp, 0, 16);
 
                        // Копирование в выходной буфер.
                        for (int i = 0; i < b; i++)
                        {
                            recvBuffer[readed++] = temp[i];
                        }
 
                        // Повтор до явной отмены.
                        // Также, выход доступен по таймауту чтения.
                    } while (!cancellationToken.IsCancellationRequested);
                }
                catch (TimeoutException)
                {
                }
                finally
                {
                    // Количество считанного.
                    received = readed;
                    // Возвращаемый результат: принято ли что-нибудь.
                    result = readed > 0;
                }
            }
            catch (TimeoutException)
            {
            }
        }
    }
    catch
    {
        result = false;
    }
 
    return result;
}
Среди видимых недостатков - огромное количество выбрасываемых TimeoutException, что изрядно засоряет лог отладчика.


Чтение потока, лежащего в основе SerialPort.
Упоминал здесь, код приводил здесь и пример программы здесь.
На всякий случай дублирую.

Идея (не реализация) способа взята в статье If you *must* use .NET System.IO.Ports.SerialPort, где автор расстраивался по поводу криворукости разработчиков .Net Framework, написавших SerialPort так, как сделано, а не по фен-шую в полном соответствии с WinAPI.
Смысл в том, что запускается асинхронное чтение потока SerialPort.BaseStream. По завершению получаем массив байт и обрабатываем. Всё якобы асинхронно, легковесно и просто.

Не по теме:

IMHO ни черта не просто, так как в .Net, версий по крайней мере <= 4.0, асинхронное чтение не остановить без закрытия потока... А ещё мне необходимы вполне конкретные таймауты.



Но рабочую реализацию запилил-таки:


Класс для таймаутов. Нужен для вызова события (callback, обратного вызова, в общем - определённого указанного кода), если обратный отсчёт времени не был остановлен/перезапущен в отведённый период времени. Аналог таймера WatchDog и прерывания UART - IDLE из области микроконтроллеров.
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using System;
using System.Threading;
 
namespace SerialStreamTest
{
    /// <summary>
    /// Таймер отсчёта таймаута
    /// </summary>
    class TimerTimeout
    {
        private Timer mTimer;
        private Action mCallback;
        private object mCallbackLocker;
 
        public TimerTimeout()
        {
            this.mTimer = new Timer(this.CallBack, null, Timeout.Infinite, Timeout.Infinite);
            this.mCallback = null;
            this.mCallbackLocker = new object();
        }
 
        private void CallBack(object state)
        {
            lock (this.mCallbackLocker)
            {
                if (this.mCallback != null)
                {
                    this.mCallback();
                }
            }
        }
 
        /// <summary>
        /// Запуск/перезапуск таймера.
        /// <paramref name="Callback"/> будет вызван через <paramref name="millisecondsTimeout"/> мс после текущего момента времени.
        /// Если таймер был запущен ранее, предыдущий <paramref name="Callback"/> отменится.
        /// </summary>
        /// <param name="millisecondsTimeout">Время, через которое будет вызван <paramref name="Callback"/>.</param>
        /// <param name="callback">Функция обратного вызова по срабатыванию таймера.</param>
        public void ReStart(int millisecondsTimeout, Action callback)
        {
            lock (this.mCallbackLocker)
            {
                this.mCallback = callback;
            }
 
            this.mTimer.Change(millisecondsTimeout, Timeout.Infinite);
        }
 
        /// <summary>
        /// Отмена отсчёта таймера.
        /// </summary>
        public void Stop()
        {
            this.mTimer.Change(Timeout.Infinite, Timeout.Infinite);
 
            lock (this.mCallbackLocker)
            {
                this.mCallback = null;
            }
        }
    }
}


Сам класс для обмена
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace SerialStreamTest
{
    /// <summary>
    /// Класс чтения/записи в последовательный порт.
    /// </summary>
    /// <remarks>
    /// Задача: записать данные в порт и принять ответ.
    /// Ответа может и не быть.
    /// Отдельные таймауты на ожидание начала приёма и на ожидание завершения приёма.
    /// 
    /// Запись работает как обычно.
    /// 
    /// Приём:
    /// Запускаем таймер отсчёта начала ответа.
    /// Входим в состояние приёма.
    /// Выход из него возможен по таймауту начала ответа и по таймауту завершения приёма.
    /// Если время начала ответа вышло, ожидание завершается и возвращается пустой массив.
    /// Если за время начала ответа этот ответ начал приниматься, ждём уже завершения приёма.
    /// </remarks>
    class SerialStream4 : IDisposable
    {
        protected SerialPort mPort;
 
        protected ConcurrentQueue<byte> mReceiveQueue;
        /// <summary>
        /// Задача для повторяющегося чтения
        /// </summary>
        protected Task mReaderTask;
        /// <summary>
        /// Токен отмены задачи повторяющегося чтения
        /// </summary>
        protected CancellationTokenSource mReaderTaskToken;
        /// <summary>
        /// Событие приёма чего-либо
        /// </summary>
        protected ManualResetEventSlim mReadEvent;
        /// <summary>
        /// Событие таймаута начала приёма
        /// </summary>
        protected ManualResetEventSlim mStartTimeoutEvent;
        /// <summary>
        /// Событие таймаута после приёма
        /// </summary>
        protected ManualResetEventSlim mEndTimeoutEvent;
        /// <summary>
        /// Таймер для отсчёта таймаута начала пакета
        /// </summary>
        protected TimerTimeout mTimerStartTimeout;
        /// <summary>
        /// Таймер для отсчёта таймаута завершения пакета
        /// </summary>
        protected TimerTimeout mTimerEndTimeout;
        /// <summary>
        /// Время ожидания завершения приёма, мс
        /// </summary>
        protected int mEndTimeout;
 
        public SerialStream4()
        {
            this.mPort = new SerialPort();
            this.mReceiveQueue = new ConcurrentQueue<byte>();
            this.mReadEvent = new ManualResetEventSlim(false);
            this.mStartTimeoutEvent = new ManualResetEventSlim(false);
            this.mEndTimeoutEvent = new ManualResetEventSlim(false);
            this.mTimerStartTimeout = new TimerTimeout();
            this.mTimerEndTimeout = new TimerTimeout();
            this.mEndTimeout = Timeout.Infinite;
        }
 
        public void Dispose()
        {
            this.Close();
        }
 
        public bool Open(string portName, int baudRate, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One)
        {
            this.mPort.PortName = portName;
            this.mPort.BaudRate = baudRate;
            this.mPort.Parity = parity;
            this.mPort.DataBits = dataBits;
            this.mPort.StopBits = stopBits;
            this.mPort.ReadTimeout = 100;
 
            this.mPort.Open();
 
            if (this.mPort.IsOpen)
            {
                this.mReaderTaskToken = new CancellationTokenSource();
                this.mReaderTask = Task.Factory.StartNew(o => this.AsyncReader((CancellationToken)o), this.mReaderTaskToken.Token, this.mReaderTaskToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
            }
 
            return this.mPort.IsOpen;
        }
 
        private void AsyncReader(CancellationToken token)
        {
            while (true)
            {
                Task<ReadedData> task = this.ReadPart(64, 100, this.mReaderTaskToken.Token);
 
                task.Wait(token);
                token.ThrowIfCancellationRequested();
 
                foreach (var b in task.Result.Array.Take(task.Result.Count))
                {
                    this.mReceiveQueue.Enqueue(b);
                }
 
                this.mReadEvent.Set();
            }
        }
 
        public void Close()
        {
            if (this.mPort.IsOpen)
            {
                try
                {
                    this.mReaderTaskToken.Cancel();
                    this.mReaderTask.Wait();
                }
                catch (AggregateException)
                {
                }
            }
 
            this.mPort.Close();
        }
 
        public bool IsOpen
        {
            get { return this.mPort.IsOpen; }
        }
 
        public void DiscardInBuffer()
        {
            byte b;
 
            while (this.mReceiveQueue.TryDequeue(out b)) ;
 
            this.mReadEvent.Reset();
        }
 
        public void Write(byte[] buffer)
        {
            Console.WriteLine("Send");
            this.mPort.Write(buffer, 0, buffer.Length);
        }
 
        public bool Read(out byte[] receivedBuffer, int startTimeout, int endTimeout, CancellationToken cancellationToken)
        {
            this.mStartTimeoutEvent.Reset();
            this.mEndTimeoutEvent.Reset();
 
            this.mEndTimeout = endTimeout;
 
            //this.mTimerStartTimeout.ReStart(startTimeout, this.mStartTimeoutEvent.Set);
            this.mTimerStartTimeout.ReStart(startTimeout, this.StartTimeoutCallback);
 
            int completedIndex = WaitHandle.WaitAny(
                new WaitHandle[] {
                    this.mEndTimeoutEvent.WaitHandle,
                    this.mStartTimeoutEvent.WaitHandle,
                    cancellationToken.WaitHandle
                });
 
            switch (completedIndex)
            {
                case 0:
                    {
                        this.mEndTimeoutEvent.Reset();
                        this.mStartTimeoutEvent.Reset();
 
                        List<byte> bytes = new List<byte>();
                        byte b;
 
                        while (this.mReceiveQueue.TryDequeue(out b))
                        {
                            bytes.Add(b);
                        }
 
                        receivedBuffer = bytes.ToArray();
 
                        return true;
                    }
                case 1:
                case 2:
                default:
                    {
                        this.mEndTimeoutEvent.Reset();
                        this.mStartTimeoutEvent.Reset();
 
                        receivedBuffer = new byte[] { };
 
                        return false;
                    }
            }
        }
 
        private Task<ReadedData> ReadPart(int expectedCount, int millisecondsTimeout, CancellationToken cancellationToken)
        {
            ReadedData data = new ReadedData();
 
            List<byte> buffer = new List<byte>();
            byte[] tempBuffer = new byte[expectedCount];
 
            data.Array = tempBuffer;
 
            Task<int> taskReader = Task<int>.Factory.FromAsync(
                this.mPort.BaseStream.BeginRead, // IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
                this.mPort.BaseStream.EndRead, // int EndRead(IAsyncResult asyncResult);
                tempBuffer,
                0,
                tempBuffer.Length,
                data);
 
            Task<ReadedData> taskContinuation = taskReader.ContinueWith<ReadedData>(x =>
            {
                int actualLength = 0;
 
                try
                {
                    actualLength = x.Result;
                    data.Count = actualLength;
                    this.mTimerStartTimeout.Stop();
 
                    //this.mTimerEndTimeout.ReStart(this.mEndTimeout, this.mEndTimeoutEvent.Set);
                    this.mTimerEndTimeout.ReStart(this.mEndTimeout, this.EndTimeoutCallback);
 
                    Console.WriteLine("Received: [{0}]", actualLength);
                }
                catch (IOException exc)
                {
                    Console.WriteLine(exc.Message);
                    data.Exception = exc;
                }
 
                return data;
            });
 
            return taskContinuation;
        }
 
        private void StartTimeoutCallback()
        {
            Console.WriteLine("Answer not received");
            this.mStartTimeoutEvent.Set();
        }
 
        private void EndTimeoutCallback()
        {
            Console.WriteLine("Answer completed");
            this.mEndTimeoutEvent.Set();
        }
 
        private class ReadedData
        {
            public byte[] Array { get; set; }
            public int Count { get; set; }
            public Exception Exception { get; set; }
 
            public ReadedData()
            {
                this.Array = new byte[] { };
                this.Count = 0;
                this.Exception = null;
            }
        }
    }
}


Пример использования
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace SerialStreamTest
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource token = new CancellationTokenSource();
 
            Task task = Task.Factory.StartNew(o => Read((CancellationToken)o), token.Token, token.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
 
            Console.ReadLine();
 
            try
            {
                token.Cancel();
                task.Wait();
            }
            catch (AggregateException)
            {
            }
        }
 
        private static void Read(CancellationToken token)
        {
            byte[] send = new byte[] { ... тут байты запроса ... };
            using (var port = new SerialStream4())
            {
                port.Open("COM28", 19200, Parity.Odd, 8, StopBits.One);
 
                while (!token.IsCancellationRequested)
                {
                    port.Write(send);
 
                    byte[] recv;
 
                    if (port.Read(out recv, 100, 4, token))
                    {
                        Console.WriteLine("[{0}] {1}", recv.Length, String.Join(" ", recv.Select(x => x.ToString("X2"))));
                    }
                }
 
                port.Close();
            }
        }
    }
}
Вложения
Тип файла: zip SerialStreamTest.zip (30.5 Кб, 123 просмотров)
Размещено в C# .Net
Просмотров 1346 Комментарии 3
Всего комментариев 3
Комментарии
  1. Старый комментарий
    Аватар для Jman
    Спасибо за статью!!! Хорошая работа!
    Запись от Jman размещена 13.03.2019 в 10:08 Jman вне форума
  2. Старый комментарий
    Аватар для Avazart
    Вообще-то с сериал портом с WinApi удобнее работать что что Вы называете "асинхронно",
    а если говорить предельно точно через OVERLAPPED ("перекрываемые операции").
    И там можно и таймауты организовать и отмену.

    Я по крайней мере так работаю на С++ если надо через WinApi.
    Запись от Avazart размещена 13.03.2019 в 23:52 Avazart вне форума
    Обновил(-а) Avazart 13.03.2019 в 23:55
  3. Старый комментарий
    Аватар для Rius
    Да, я уже перешёл на WinAPI.
    Статья Serial Communication in Win32 выложена тут: «Не отвечает» com порт
    Обращение к функциям WinAPI тут: Правильный подход обмена данных с устройствами через COM-порт. Целостность пакетов и производительность обмена
    Запись от Rius размещена 14.03.2019 в 05:40 Rius вне форума
    Обновил(-а) Rius 14.03.2019 в 06:15
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru