33 private static readonly TimeSpan IdleReadWait = TimeSpan.FromSeconds(1);
34 private static readonly TimeSpan ReconnectDelay = TimeSpan.FromMilliseconds(250);
41 private const int ReadFaultsBeforeReconnect = 3;
44 private readonly
byte _address;
45 private readonly ILogger _logger;
47 private DateTime _lastValidCommand = DateTime.MinValue;
48 private int _consecutiveReadFaults;
61 _connection = connection ??
throw new ArgumentNullException(nameof(connection));
107 public async Task
RunAsync(CancellationToken token)
109 if (!_connection.IsOpen)
111 await _connection.Open().ConfigureAwait(
false);
117 _connection.DiscardBuffersBeforeWrite =
false;
118 _lastValidCommand = DateTime.UtcNow;
120 _logger?.LogInformation(
121 "Line quality responder listening at address {Address} on {Connection}, {BaudRate} baud",
122 _address, _connection, _connection.BaudRate);
124 while (!token.IsCancellationRequested)
126 var command = await ReadCommand(token).ConfigureAwait(
false);
129 RevertBaudRateIfIdle();
133 _lastValidCommand = DateTime.UtcNow;
137 await Respond(command, token).ConfigureAwait(
false);
139 catch (OperationCanceledException)
143 catch (Exception exception)
145 _logger?.LogError(exception,
"Failed to reply to a line quality command");
154 private async Task<IncomingMessage> ReadCommand(CancellationToken token)
156 var buffer =
new Collection<byte>();
158 if (!await WaitForStartOfMessage(buffer, token).ConfigureAwait(
false))
163 if (!await Bus.WaitForMessageLength(_connection, buffer, token).ConfigureAwait(
false))
168 if (!await Bus.WaitForRestOfMessage(_connection, buffer, Bus.ExtractMessageLength(buffer), token)
169 .ConfigureAwait(
false))
177 command =
new IncomingMessage(buffer.ToArray(), ClearTextChannel.Instance);
179 catch (Exception exception)
183 _logger?.LogDebug(exception,
"Discarded an unparsable frame");
189 _logger?.LogDebug(
"Discarded a frame that failed its integrity check");
193 return command.Address == _address ? command :
null;
207 private async Task<bool> WaitForStartOfMessage(Collection<byte> buffer, CancellationToken token)
209 var deadline = DateTime.UtcNow + IdleReadWait;
210 var readBuffer =
new byte[1];
212 while (DateTime.UtcNow < deadline)
214 token.ThrowIfCancellationRequested();
219 bytesRead = await ReadWithTimeout(readBuffer, IdleReadWait, token).ConfigureAwait(
false);
221 catch (Exception exception)
223 await HandleReadFault(exception, token).ConfigureAwait(
false);
227 _consecutiveReadFaults = 0;
230 if (bytesRead == 0)
return false;
232 if (readBuffer[0] != Message.StartOfMessage)
continue;
234 buffer.Add(readBuffer[0]);
253 private async Task<int> ReadWithTimeout(
byte[] buffer, TimeSpan timeout, CancellationToken token)
255 using var timeoutSource =
new CancellationTokenSource(timeout);
256 using var linkedSource =
257 CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token);
261 return await _connection.ReadAsync(buffer, linkedSource.Token).ConfigureAwait(
false);
263 catch (Exception exception) when (timeoutSource.IsCancellationRequested &&
264 IsTimeoutSignal(exception))
268 token.ThrowIfCancellationRequested();
276 private static bool IsTimeoutSignal(Exception exception) =>
277 exception is OperationCanceledException || exception is TimeoutException;
279 private async Task HandleReadFault(Exception exception, CancellationToken token)
283 if (token.IsCancellationRequested)
return;
285 _consecutiveReadFaults++;
287 _logger?.LogWarning(exception,
"Serial read failed ({Count} in a row)",
288 _consecutiveReadFaults);
290 if (_consecutiveReadFaults < ReadFaultsBeforeReconnect)
return;
292 _consecutiveReadFaults = 0;
293 await ReopenConnection(token).ConfigureAwait(
false);
299 private async Task ReopenConnection(CancellationToken token)
301 int baudRate = _connection.BaudRate;
305 await _connection.Close().ConfigureAwait(
false);
306 await Task.Delay(ReconnectDelay, token).ConfigureAwait(
false);
307 await _connection.Open().ConfigureAwait(
false);
309 _connection.SetBaudRate(baudRate);
310 _connection.DiscardBuffersBeforeWrite =
false;
315 "Reopened the connection at {BaudRate} baud to clear repeated read failures",
318 catch (OperationCanceledException)
322 catch (Exception exception)
324 _logger?.LogError(exception,
"Could not reopen the connection after read failures");
328 private async Task Respond(IncomingMessage command, CancellationToken token)
332 await SendReply(command,
new Nak(
ErrorCode.UnknownCommandCode), token).ConfigureAwait(
false);
336 var payload = command.
Payload.AsSpan();
338 if (EchoRequest.TryParse(payload, out var echoRequest))
340 await HandleEcho(command, echoRequest, token).ConfigureAwait(
false);
344 if (BaudRateChange.TryParse(payload, out var baudRateChange))
346 await HandleBaudRateChange(command, baudRateChange, token).ConfigureAwait(
false);
350 await SendReply(command,
new Nak(
ErrorCode.UnknownCommandCode), token).ConfigureAwait(
false);
353 private async Task HandleEcho(IncomingMessage command, EchoRequest request, CancellationToken token)
357 if (!LineQualityProtocol.IsSupportedPattern(request.Pattern))
359 reply =
new EchoResponse(request.SequenceNumber,
EchoStatus.UnsupportedPattern);
361 else if (request.DeclaredPayloadLength > LineQualityProtocol.MaxPayloadLength)
363 reply =
new EchoResponse(request.SequenceNumber,
EchoStatus.LengthError);
365 else if (!request.IsLengthConsistent)
375 reply =
new EchoResponse(request.SequenceNumber, request.Data);
378 await SendReply(command, reply, token).ConfigureAwait(
false);
380 OnExchangeCompleted(
new LineQualityExchangeEventArgs(request.SequenceNumber, request.Pattern,
381 request.Data.Length, (reply as EchoResponse)?.Status, _connection.BaudRate));
384 private async Task HandleBaudRateChange(IncomingMessage command, BaudRateChange request,
385 CancellationToken token)
392 newBaudRate = LineQualityProtocol.ToBaudRate(request.BaudRateId);
395 catch (ArgumentOutOfRangeException)
397 _logger?.LogWarning(
"Rejected unsupported baud rate ID {BaudRateId}", request.BaudRateId);
400 int writtenBytes = await SendReply(command,
401 new BaudRateChangeAck(request.SequenceNumber, status), token).ConfigureAwait(
false);
407 await _connection.WaitForTransmitCompleteAsync(writtenBytes, token).ConfigureAwait(
false);
410 _connection.SetBaudRate(newBaudRate);
411 _lastValidCommand = DateTime.UtcNow;
413 _logger?.LogInformation(
"Switched to {BaudRate} baud", newBaudRate);
414 BaudRateChanged?.Invoke(
this,
new LineQualityBaudRateChangedEventArgs(newBaudRate,
false));
417 private async Task<int> SendReply(IncomingMessage command, PayloadData reply, CancellationToken token)
419 var message =
new OutgoingReply(command, reply).BuildMessage(ClearTextChannel.Instance);
421 token.ThrowIfCancellationRequested();
422 await _connection.WriteAsync(message).ConfigureAwait(
false);
424 return message.Length;
427 private void RevertBaudRateIfIdle()
432 if (_connection.BaudRate == fallbackRate)
return;
436 "No valid command for {Timeout}; reverting from {Current} to {Fallback} baud",
439 _connection.SetBaudRate(fallbackRate);
440 _lastValidCommand = DateTime.UtcNow;
442 BaudRateChanged?.Invoke(
this,
new LineQualityBaudRateChangedEventArgs(fallbackRate,
true));
445 private void OnExchangeCompleted(LineQualityExchangeEventArgs args) =>