问题
Hi I have a simple communication on serial port well all is according to book and documentation so open port method looks like this:
public SerialPort OpenPort(string portName)
{
Port = new SerialPort(portName, BaudRate);
try
{
Port.Open();
Port.DtrEnable = true;
Port.RtsEnable = true;
Port.DataReceived += DataReceivedEvent;
}
catch (Exception e)
{
Console.WriteLine($"ERRROR: {e.Message}");
}
return Port;
}
Here we have an event on data read:
private async void DataReceivedEvent(object sender, SerialDataReceivedEventArgs e)
{
var data = new byte[Port.BytesToRead];
await Port.BaseStream.ReadAsync(data, 0, data.Length);
Response = data;
isFinished = true;
}
Well all is fine and dandy, but now i want to send a message on demand and store response in a property, also i want to add cancellation token on that task timeout. So i came up with this method:
public async Task SendMessenge(byte[] messange)
{
var cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;
cancellationTokenSource.CancelAfter(5000);
token.ThrowIfCancellationRequested();
isFinished = false;
try
{
Task worker = Task.Run(() =>
{
while (!isFinished)
{
}
}, token);
await Port.BaseStream.WriteAsync(messange, 0, messange.Length, token);
await worker;
}
catch (OperationCanceledException e)
{
throw new OperationCanceledException(e.Message, e, token);
}
}
Problem is with this while loop, if it is task it goes into endless loop, and it does not capture timeout token, if i put it outside a task and remove worker it works but im loosing cancellation token. I guess i could do some manual countdown like:
double WaitTimeout = Timeout + DateAndTime.Now.TimeOfDay.TotalMilliseconds;
while (!(DateAndTime.Now.TimeOfDay.TotalMilliseconds >= WaitTimeout)|| !isFalse)
But it looks ugly.
So i think my basic question is how to effectively await for event to response and get a timeout?
回答1:
Read data in a loop after write operation until get a full response. But you need to use synchronous API and Task.Run()
as current version of the asynchronous API ignores SerialPort
timeout properties completely and CancellationToken
in Task based API almost completely.
Excerpt from the SerialPort.ReadTimeout Microsoft Docs that is relevant to SerialPort.BaseStream.ReadAsync()
because it uses default implementation Stream.ReadAsync()
:
This property does not affect the BeginRead method of the stream returned by the BaseStream property.
Example implementation using synchronous API and dynamic timeout properties update:
static byte[] SendMessage(byte[] message, TimeSpan timeout)
{
// Use stopwatch to update SerialPort.ReadTimeout and SerialPort.WriteTimeout
// as we go.
var stopwatch = Stopwatch.StartNew();
// Organize critical section for logical operations using some standard .NET tool.
lock (_syncRoot)
{
var originalWriteTimeout = _serialPort.WriteTimeout;
var originalReadTimeout = _serialPort.ReadTimeout;
try
{
// Start logical request.
_serialPort.WriteTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
_serialPort.Write(message, 0, message.Length);
// Expected response length. Look for the constant value from
// the device communication protocol specification or extract
// from the response header (first response bytes) if there is
// any specified in the protocol.
int count = ...;
byte[] buffer = new byte[count];
int offset = 0;
// Loop until we recieve a full response.
while (count > 0)
{
_serialPort.ReadTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
var readCount = _serialPort.Read(buffer, offset, count);
offset += readCount;
count -= readCount;
}
return buffer;
}
finally
{
// Restore SerialPort state.
_serialPort.ReadTimeout = originalReadTimeout;
_serialPort.WriteTimeout = originalWriteTimeout;
}
}
}
And example usage:
byte[] request = ...;
TimeSpan timeout = ...;
var sendTask = Task.Run(() => SendMessage(request, timeout));
try
{
await await Task.WhenAny(sendTask, Task.Delay(timeout));
}
catch (TaskCanceledException)
{
throw new TimeoutException();
}
byte[] response = await sendTask;
You can do similar thing with CancellationToken
instance and use CancellationToken.ThrowIfCancellationRequested()
between read and write operations but you have to make sure that proper timeouts are set on SerialPort
or otherwise Thread pool thread will hang forever possible holding a lock. As far as I know you can't utilize CancellationToken.Register()
because there is no SerialPort
method to call to cancel an operation.
For more information check:
- Top 5 SerialPort Tips article by Kim Hamilton
- Recommended asynchronous usage pattern of SerialPort, Document that CancellationToken in Stream.ReadAsync() is advisory and NetworkStream.ReadAsync/WriteAsync ignores CancellationToken related issues on .NET GitHub
- Should I expose asynchronous wrappers for synchronous methods? article by Stephen Toub
来源:https://stackoverflow.com/questions/53335736/c-sharp-await-event-and-timeout-in-serial-port-communication