Dreamine.PLC.Core 1.0.1
Dreamine.PLC.Core 산업 자동화 및 I/O 기능을 제공합니다.
로딩중...
검색중...
일치하는것 없음
PlcSimulatorServer.cs
이 파일의 문서화 페이지로 가기
1using System.Net;
2using System.Net.Sockets;
5
7
16public sealed class PlcSimulatorServer : IAsyncDisposable
17{
35 private readonly InMemoryPlcMemory _memory;
44 private readonly DefaultPlcAddressParser _addressParser = new();
53 private readonly List<Task> _clientTasks = [];
62 private readonly object _syncRoot = new();
71 private CancellationTokenSource? _cts;
80 private TcpListener? _listener;
89 private Task? _acceptTask;
90
116 : this(options, new InMemoryPlcMemory())
117 {
118 }
119
152 {
153 _options = options ?? throw new ArgumentNullException(nameof(options));
154 _memory = memory ?? throw new ArgumentNullException(nameof(memory));
155 }
156
165 public event EventHandler<string>? StatusChanged;
166
175 public bool IsRunning => _listener is not null;
176
200 public Task StartAsync(CancellationToken cancellationToken = default)
201 {
202 if (_listener is not null)
203 {
204 return Task.CompletedTask;
205 }
206
207 var address = ParseAddress(_options.Host);
208 _listener = new TcpListener(address, _options.Port);
209 _listener.Start();
210 _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
211 _acceptTask = Task.Run(() => AcceptLoopAsync(_cts.Token), CancellationToken.None);
212 StatusChanged?.Invoke(this, $"PLC simulator server started. {_options.Host}:{_options.Port}");
213 return Task.CompletedTask;
214 }
215
232 public async Task StopAsync()
233 {
234 if (_listener is null)
235 {
236 return;
237 }
238
239 _cts?.Cancel();
240 _listener.Stop();
241 _listener = null;
242
243 if (_acceptTask is not null)
244 {
245 try
246 {
247 await _acceptTask.ConfigureAwait(false);
248 }
249 catch (OperationCanceledException)
250 {
251 }
252 catch (ObjectDisposedException)
253 {
254 }
255 }
256
257 Task[] clientTasks;
258 lock (_syncRoot)
259 {
260 clientTasks = _clientTasks.ToArray();
261 _clientTasks.Clear();
262 }
263
264 try
265 {
266 await Task.WhenAll(clientTasks).ConfigureAwait(false);
267 }
268 catch
269 {
270 }
271
272 _cts?.Dispose();
273 _cts = null;
274 StatusChanged?.Invoke(this, "PLC simulator server stopped.");
275 }
276
292 public async ValueTask DisposeAsync()
293 {
294 await StopAsync().ConfigureAwait(false);
295 }
296
319 private async Task AcceptLoopAsync(CancellationToken cancellationToken)
320 {
321 while (!cancellationToken.IsCancellationRequested && _listener is not null)
322 {
323 TcpClient client;
324
325 try
326 {
327 client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
328 }
329 catch (OperationCanceledException)
330 {
331 break;
332 }
333 catch (ObjectDisposedException)
334 {
335 break;
336 }
337
338 var task = Task.Run(() => HandleClientAsync(client, cancellationToken), CancellationToken.None);
339 lock (_syncRoot)
340 {
341 _clientTasks.RemoveAll(static item => item.IsCompleted);
342 _clientTasks.Add(task);
343 }
344 }
345 }
346
376 private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
377 {
378 using (client)
379 using (var stream = client.GetStream())
380 using (var reader = new StreamReader(stream))
381 using (var writer = new StreamWriter(stream) { AutoFlush = true })
382 {
383 StatusChanged?.Invoke(this, "PLC simulator client connected.");
384
385 while (!cancellationToken.IsCancellationRequested)
386 {
387 var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
388 if (line is null)
389 {
390 break;
391 }
392
393 var response = ExecuteLine(line);
394 await writer.WriteLineAsync(response.AsMemory(), cancellationToken).ConfigureAwait(false);
395 }
396 }
397
398 StatusChanged?.Invoke(this, "PLC simulator client disconnected.");
399 }
400
423 private string ExecuteLine(string line)
424 {
425 try
426 {
427 var parts = line.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
428 if (parts.Length < 3)
429 {
430 return PlcSimulatorProtocol.Error("Invalid command format.");
431 }
432
433 var command = parts[0].ToUpperInvariant();
434 var addressResult = _addressParser.Parse(parts[1]);
435 if (!addressResult.IsSuccess)
436 {
437 return PlcSimulatorProtocol.Error(addressResult.Message ?? "Invalid PLC address.");
438 }
439
440 var address = addressResult.Value;
441 var argument = parts[2];
442
443 return command switch
444 {
449 _ => PlcSimulatorProtocol.Error($"Unsupported command: {command}")
450 };
451 }
452 catch (Exception ex)
453 {
454 return PlcSimulatorProtocol.Error(ex.Message);
455 }
456 }
457
487 private string ExecuteReadBits(Abstractions.Devices.PlcAddress address, string argument)
488 {
489 if (!TryParseCount(argument, out var count, out var error))
490 {
491 return PlcSimulatorProtocol.Error(error);
492 }
493
494 var result = _memory.ReadBits(address, count);
495 return result.IsSuccess && result.Value is not null
497 : PlcSimulatorProtocol.Error(result.Message ?? "Read bits failed.");
498 }
499
529 private string ExecuteReadWords(Abstractions.Devices.PlcAddress address, string argument)
530 {
531 if (!TryParseCount(argument, out var count, out var error))
532 {
533 return PlcSimulatorProtocol.Error(error);
534 }
535
536 var result = _memory.ReadWords(address, count);
537 return result.IsSuccess && result.Value is not null
539 : PlcSimulatorProtocol.Error(result.Message ?? "Read words failed.");
540 }
541
578 private static bool TryParseCount(string text, out int count, out string error)
579 {
580 if (!int.TryParse(text, out count))
581 {
582 error = $"Invalid read count: {text}";
583 return false;
584 }
585
586 if (count <= 0)
587 {
588 error = "Read count must be greater than zero.";
589 return false;
590 }
591
592 error = string.Empty;
593 return true;
594 }
595
625 private string ExecuteWriteBits(Abstractions.Devices.PlcAddress address, string argument)
626 {
627 var values = PlcSimulatorProtocol.ParseBits(argument);
628 var result = _memory.WriteBits(address, values);
629 return result.IsSuccess ? PlcSimulatorProtocol.Ok() : PlcSimulatorProtocol.Error(result.Message ?? "Write bits failed.");
630 }
631
661 private string ExecuteWriteWords(Abstractions.Devices.PlcAddress address, string argument)
662 {
663 var values = PlcSimulatorProtocol.ParseWords(argument);
664 var result = _memory.WriteWords(address, values);
665 if (!result.IsSuccess)
666 {
667 return PlcSimulatorProtocol.Error(result.Message ?? "Write words failed.");
668 }
669
670 ApplyAutoWordResponse(address, values);
671 return PlcSimulatorProtocol.Ok();
672 }
673
696 private void ApplyAutoWordResponse(Abstractions.Devices.PlcAddress writtenAddress, IReadOnlyList<short> values)
697 {
698 if (!_options.EnableAutoWordResponse || values.Count != 1)
699 {
700 return;
701 }
702
703 var triggerResult = _addressParser.Parse(_options.AutoResponseTriggerAddress);
704 var responseResult = _addressParser.Parse(_options.AutoResponseAddress);
705 if (!triggerResult.IsSuccess || !responseResult.IsSuccess)
706 {
707 return;
708 }
709
710 var triggerAddress = triggerResult.Value;
711 if (writtenAddress.DeviceType != triggerAddress.DeviceType || writtenAddress.Offset != triggerAddress.Offset)
712 {
713 return;
714 }
715
716 var rawResponseValue = values[0] + _options.AutoResponseIncrement;
717 if (rawResponseValue is < short.MinValue or > short.MaxValue)
718 {
719 StatusChanged?.Invoke(this, $"Auto response skipped: value overflow. {_options.AutoResponseAddress}={rawResponseValue}");
720 return;
721 }
722
723 var responseValue = (short)rawResponseValue;
724 _memory.WriteWords(responseResult.Value, [responseValue]);
725 StatusChanged?.Invoke(this, $"Auto response: {_options.AutoResponseAddress}={responseValue}");
726 }
727
750 private static IPAddress ParseAddress(string host)
751 {
752 if (string.IsNullOrWhiteSpace(host) || host == "*" || host == "+")
753 {
754 return IPAddress.Any;
755 }
756
757 return IPAddress.TryParse(host, out var address) ? address : IPAddress.Any;
758 }
759}
static string FormatWords(IEnumerable< short > values)
static string FormatBits(IEnumerable< bool > values)
Task StartAsync(CancellationToken cancellationToken=default)
void ApplyAutoWordResponse(Abstractions.Devices.PlcAddress writtenAddress, IReadOnlyList< short > values)
PlcSimulatorServer(PlcSimulatorServerOptions options, InMemoryPlcMemory memory)
string ExecuteWriteWords(Abstractions.Devices.PlcAddress address, string argument)
async Task AcceptLoopAsync(CancellationToken cancellationToken)
static bool TryParseCount(string text, out int count, out string error)
string ExecuteReadBits(Abstractions.Devices.PlcAddress address, string argument)
PlcSimulatorServer(PlcSimulatorServerOptions options)
async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
string ExecuteWriteBits(Abstractions.Devices.PlcAddress address, string argument)
readonly DefaultPlcAddressParser _addressParser
string ExecuteReadWords(Abstractions.Devices.PlcAddress address, string argument)
readonly PlcSimulatorServerOptions _options