diff --git a/Sharp7.Monitor/Program.cs b/Sharp7.Monitor/Program.cs index c71a020..04a8630 100644 --- a/Sharp7.Monitor/Program.cs +++ b/Sharp7.Monitor/Program.cs @@ -1,160 +1,59 @@ -using System.ComponentModel; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; -using System.Text; -using JetBrains.Annotations; -using Sharp7.Read; -using Sharp7.Rx; -using Sharp7.Rx.Enums; -using Spectre.Console; +using System.Text; using Spectre.Console.Cli; -Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; +namespace Sharp7.Monitor; -using var cancellationSource = new CancellationTokenSource(); - -Console.CancelKeyPress += OnCancelKeyPress; -AppDomain.CurrentDomain.ProcessExit += onProcessExit; - - -void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) +internal class Program { - if (!cancellationSource.IsCancellationRequested) - // NOTE: cancel event, don't terminate the process - e.Cancel = true; + private static readonly CancellationTokenSource cts = new(); - cancellationSource.Cancel(); -} - -void onProcessExit(object? sender, EventArgs e) -{ - if (cancellationSource.IsCancellationRequested) + public static async Task Main(string[] args) { - // NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event handler upon cancellation of the `cancellationSource`) - return; - } + Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; - cancellationSource.Cancel(); -} -await using var t = cancellationSource.Token.Register(() => Console.WriteLine("Cancelled!")); + Console.CancelKeyPress += OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; -try -{ - var app = new CommandApp(); - app.WithData(cancellationSource.Token); - return await app.RunAsync(args); -} -finally -{ - Console.WriteLine("all done"); - AppDomain.CurrentDomain.ProcessExit -= onProcessExit; - Console.CancelKeyPress -= OnCancelKeyPress; -} + try + { + var app = + new CommandApp() + .WithData(cts.Token) + .WithDescription("This program connects to a PLC and reads the variables specified as command line arguments."); -internal sealed class ReadPlcCommand : AsyncCommand -{ - public override async Task ExecuteAsync(CommandContext context, Settings settings) - { - var token = (CancellationToken) (context.Data ?? CancellationToken.None); - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine($"Establishing connection to plc [green]{settings.PlcIp}[/], CPU [green]{settings.CpuMpiAddress}[/], rack [green]{settings.RackNumber}[/]."); - using var plc = new Sharp7Plc(settings.PlcIp, settings.RackNumber, settings.CpuMpiAddress); + app.Configure(config => { config.SetApplicationName("s7mon.exe"); }); - await plc.InitializeAsync(); - - // Connect - await AnsiConsole.Status() - .Spinner(Spinner.Known.BouncingBar) - .StartAsync("Connecting...", async ctx => - { - var lastState = ConnectionState.Initial; - ctx.Status(lastState.ToString()); - - while (!token.IsCancellationRequested) - { - var state = await plc.ConnectionState.FirstAsync(s => s != lastState).ToTask(token); - ctx.Status(state.ToString()); - - if (state == ConnectionState.Connected) - return; - } - }); - - if (token.IsCancellationRequested) + return await app.RunAsync(args); + } + catch (OperationCanceledException) + { return 0; - - // Create a table - var table = new Table(); - - table.AddColumn("Variable"); - table.AddColumn("Value"); - - foreach (var variable in settings.Variables) - { - table.AddRow(variable, ""); } - - await AnsiConsole.Live(table) - .StartAsync(async ctx => - { - int i = 0; - while (!token.IsCancellationRequested) - { - table.Rows.Update(0, 1, new Text((++i).ToString())); - ctx.Refresh(); - await Task.Delay(1000, token); - - } - }); - - - - //for (int i = 0; i < 10; i++) - //{ - // await plc.SetValue($"DB{db}.Int6", (short)i); - // var value = await plc.GetValue($"DB{db}.Int6"); - // value.Dump(); - - // await Task.Delay(200); - //} - - - // AnsiConsole.MarkupLine($"Total file size for [green]{searchPattern}[/] files in [green]{searchPath}[/]: [blue]{totalFileSize:N0}[/] bytes"); - - return 0; + finally + { + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; + Console.CancelKeyPress -= OnCancelKeyPress; + } } - [NoReorder] - public sealed class Settings : CommandSettings + private static void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) { - [Description("IP address of S7")] - [CommandArgument(0, "")] - public string PlcIp { get; init; } + if (!cts.IsCancellationRequested) + // NOTE: cancel event, don't terminate the process + e.Cancel = true; - [CommandArgument(1, "[variables]")] - [Description("Variables to read from S7, like Db200.Int4.\r\nFor format description see https://github.com/evopro-ag/Sharp7Reactive.")] - public string[] Variables { get; init; } + cts.Cancel(); + } - [CommandOption("-c|--cpu")] - [Description("CPU MPI address of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] - [DefaultValue(0)] - public int CpuMpiAddress { get; init; } - - [CommandOption("-r|--rack")] - [Description("Rack number of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] - [DefaultValue(0)] - public int RackNumber { get; init; } - - public override ValidationResult Validate() + private static void OnProcessExit(object? sender, EventArgs e) + { + if (cts.IsCancellationRequested) { - if (!StringHelper.IsValidIp4(PlcIp)) - return ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); - - if (Variables == null || Variables.Length == 0) - return ValidationResult.Error("Please supply at least one variable to read"); - - return ValidationResult.Success(); + // NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event handler upon cancellation of the `cancellationSource`) + return; } + + cts.Cancel(); } } \ No newline at end of file diff --git a/Sharp7.Monitor/Properties/launchSettings.json b/Sharp7.Monitor/Properties/launchSettings.json index ad0a3d0..5bd1a19 100644 --- a/Sharp7.Monitor/Properties/launchSettings.json +++ b/Sharp7.Monitor/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Sharp7.Monitor": { "commandName": "Project", - "commandLineArgs": "10.30.110.62 DB2050.String10.5" + "commandLineArgs": "10.30.110.62 DB2050.Bit0.1 DB2050.Byte1 DB2050.Byte2.4 DB2050.Int6 DB2050.UInt8 DB2050.DInt10 DB2050.UDInt14 DB2050.LInt18 DB2050.ULInt26 DB2050.Real34 DB2050.LReal38 DB2050.String50.20 DB2050.WString80.20 DB2050.Byte130.20 " } } } \ No newline at end of file diff --git a/Sharp7.Monitor/ReadPlcCommand.cs b/Sharp7.Monitor/ReadPlcCommand.cs new file mode 100644 index 0000000..af52cda --- /dev/null +++ b/Sharp7.Monitor/ReadPlcCommand.cs @@ -0,0 +1,205 @@ +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using JetBrains.Annotations; +using Sharp7.Read; +using Sharp7.Rx; +using Sharp7.Rx.Enums; +using Sharp7.Rx.Interfaces; +using Spectre.Console; +using Spectre.Console.Cli; +using Spectre.Console.Rendering; + +internal sealed class ReadPlcCommand : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + var token = (CancellationToken) (context.Data ?? CancellationToken.None); + + try + { + await RunProgram(settings, token); + } + catch (TaskCanceledException) + { + } + + return 0; + } + + private static IRenderable FormatCellData(VariableRecord record) + { + if (record.Value is IRenderable renderable) + return renderable; + + if (record.Value is Exception ex) + return new Text(ex.Message, CustomStyles.Error); + + if (record.Value is byte[] byteArray) + { + var text = string.Join(" ", byteArray.Select(b => $"0x{b:X2}")); + return new Text(text); + } + + return new Text(record.Value.ToString() ?? ""); + } + + private static async Task RunProgram(Settings settings, CancellationToken token) + { + AnsiConsole.MarkupLine($"Connecting to plc [green]{settings.PlcIp}[/], CPU [green]{settings.CpuMpiAddress}[/], rack [green]{settings.RackNumber}[/]."); + + using var plc = new Sharp7Plc(settings.PlcIp, settings.RackNumber, settings.CpuMpiAddress); + + await plc.TriggerConnection(token); + + // Connect + await AnsiConsole.Status() + .Spinner(Spinner.Known.BouncingBar) + .StartAsync("Connecting...", async ctx => + { + var lastState = ConnectionState.Initial; + ctx.Status(lastState.ToString()); + + while (!token.IsCancellationRequested) + { + var state = await plc.ConnectionState.FirstAsync(s => s != lastState).ToTask(token); + ctx.Status(state.ToString()); + + if (state == ConnectionState.Connected) + return; + } + }); + + token.ThrowIfCancellationRequested(); + + using var variableContainer = VariableContainer.Initialize(plc, settings.Variables); + + // Create a table + var table = new Table + { + Border = TableBorder.Rounded, + BorderStyle = new Style(foreground: Color.DarkGreen) + }; + + table.AddColumn("Variable"); + table.AddColumn("Value"); + + foreach (var record in variableContainer.VariableRecords) + table.AddRow(record.Address, "[gray]init[/]"); + + await AnsiConsole.Live(table) + .StartAsync(async ctx => + { + while (!token.IsCancellationRequested) + { + foreach (var record in variableContainer.VariableRecords) + table.Rows.Update( + record.RowIdx, 1, + FormatCellData(record) + ); + + ctx.Refresh(); + + await Task.Delay(100, token); + } + }); + } + + [NoReorder] + public sealed class Settings : CommandSettings + { + [Description("IP address of S7")] + [CommandArgument(0, "")] + public required string PlcIp { get; init; } + + [CommandArgument(1, "[variables]")] + [Description("Variables to read from S7, like Db200.Int4.\r\nFor format description see https://github.com/evopro-ag/Sharp7Reactive.")] + public required string[] Variables { get; init; } + + [CommandOption("-c|--cpu")] + [Description("CPU MPI address of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] + [DefaultValue(0)] + public int CpuMpiAddress { get; init; } + + [CommandOption("-r|--rack")] + [Description("Rack number of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] + [DefaultValue(0)] + public int RackNumber { get; init; } + + public override ValidationResult Validate() + { + if (!StringHelper.IsValidIp4(PlcIp)) + return ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); + + if (Variables == null || Variables.Length == 0) + return ValidationResult.Error("Please supply at least one variable to read"); + + return ValidationResult.Success(); + } + } +} + +public static class CustomStyles +{ + public static Style Error { get; } = new Style(foreground: Color.Red); + public static Style Note { get; } = new(foreground: Color.DarkSlateGray1); +} + +public class VariableContainer : IDisposable +{ + private readonly IDisposable subscriptions; + + private VariableContainer(IReadOnlyList variableRecords, IDisposable subscriptions) + { + this.subscriptions = subscriptions; + VariableRecords = variableRecords; + } + + public IReadOnlyList VariableRecords { get; } + + public void Dispose() + { + subscriptions.Dispose(); + } + + public static VariableContainer Initialize(IPlc plc, IReadOnlyList variables) + { + var records = variables + .Select((v, i) => new VariableRecord + { + Address = v, + RowIdx = i, + Value = new Text("init", CustomStyles.Note) + }) + .ToList(); + + var disposables = new CompositeDisposable(); + foreach (var rec in records) + { + try + { + var disp = + plc.CreateNotification(rec.Address, TransmissionMode.OnChange) + .Subscribe( + data => rec.Value = data, + ex => rec.Value = new Text(ex.Message, CustomStyles.Error) + ); + disposables.Add(disp); + } + catch (Exception e) + { + rec.Value = new Text(e.Message, CustomStyles.Error); + } + } + + return new VariableContainer(records, disposables); + } +} + +public class VariableRecord +{ + public required string Address { get; init; } + public required int RowIdx { get; init; } + public object Value { get; set; } +} \ No newline at end of file diff --git a/Sharp7.Monitor/Sharp7.Monitor.csproj b/Sharp7.Monitor/Sharp7.Monitor.csproj index cd2cdac..b563862 100644 --- a/Sharp7.Monitor/Sharp7.Monitor.csproj +++ b/Sharp7.Monitor/Sharp7.Monitor.csproj @@ -5,13 +5,14 @@ net8.0 enable enable + s7mon - - + +