Implement basic funtionality

This commit is contained in:
Peter Butzhammer
2024-04-28 20:09:13 +02:00
parent 7e69e78036
commit 24f79b12ea
4 changed files with 246 additions and 141 deletions

View File

@@ -1,160 +1,59 @@
using System.ComponentModel; using System.Text;
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 Spectre.Console.Cli; using Spectre.Console.Cli;
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; namespace Sharp7.Monitor;
using var cancellationSource = new CancellationTokenSource(); internal class Program
Console.CancelKeyPress += OnCancelKeyPress;
AppDomain.CurrentDomain.ProcessExit += onProcessExit;
void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{ {
if (!cancellationSource.IsCancellationRequested) private static readonly CancellationTokenSource cts = new();
// NOTE: cancel event, don't terminate the process
e.Cancel = true;
cancellationSource.Cancel(); public static async Task<int> Main(string[] args)
}
void onProcessExit(object? sender, EventArgs e)
{
if (cancellationSource.IsCancellationRequested)
{ {
// NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event handler upon cancellation of the `cancellationSource`) Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;
return;
}
cancellationSource.Cancel();
}
await using var t = cancellationSource.Token.Register(() => Console.WriteLine("Cancelled!")); Console.CancelKeyPress += OnCancelKeyPress;
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
try try
{ {
var app = new CommandApp<ReadPlcCommand>(); var app =
app.WithData(cancellationSource.Token); new CommandApp<ReadPlcCommand>()
return await app.RunAsync(args); .WithData(cts.Token)
} .WithDescription("This program connects to a PLC and reads the variables specified as command line arguments.");
finally
{
Console.WriteLine("all done");
AppDomain.CurrentDomain.ProcessExit -= onProcessExit;
Console.CancelKeyPress -= OnCancelKeyPress;
}
internal sealed class ReadPlcCommand : AsyncCommand<ReadPlcCommand.Settings> app.Configure(config => { config.SetApplicationName("s7mon.exe"); });
{
public override async Task<int> 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);
await plc.InitializeAsync(); return await app.RunAsync(args);
}
// Connect catch (OperationCanceledException)
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 0; return 0;
// Create a table
var table = new Table();
table.AddColumn("Variable");
table.AddColumn("Value");
foreach (var variable in settings.Variables)
{
table.AddRow(variable, "");
} }
finally
await AnsiConsole.Live(table) {
.StartAsync(async ctx => AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
{ Console.CancelKeyPress -= OnCancelKeyPress;
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<short>($"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;
} }
[NoReorder] private static void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
public sealed class Settings : CommandSettings
{ {
[Description("IP address of S7")] if (!cts.IsCancellationRequested)
[CommandArgument(0, "<IP address>")] // NOTE: cancel event, don't terminate the process
public string PlcIp { get; init; } e.Cancel = true;
[CommandArgument(1, "[variables]")] cts.Cancel();
[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; }
[CommandOption("-c|--cpu")] private static void OnProcessExit(object? sender, EventArgs e)
[Description("CPU MPI address of S7 instance.\r\nSee https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot.\r\n")] {
[DefaultValue(0)] if (cts.IsCancellationRequested)
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)) // 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 ValidationResult.Error($"\"{PlcIp}\" is not a valid IP V4 address"); return;
if (Variables == null || Variables.Length == 0)
return ValidationResult.Error("Please supply at least one variable to read");
return ValidationResult.Success();
} }
cts.Cancel();
} }
} }

View File

@@ -2,7 +2,7 @@
"profiles": { "profiles": {
"Sharp7.Monitor": { "Sharp7.Monitor": {
"commandName": "Project", "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 "
} }
} }
} }

View File

@@ -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<ReadPlcCommand.Settings>
{
public override async Task<int> 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, "<IP address>")]
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<VariableRecord> variableRecords, IDisposable subscriptions)
{
this.subscriptions = subscriptions;
VariableRecords = variableRecords;
}
public IReadOnlyList<VariableRecord> VariableRecords { get; }
public void Dispose()
{
subscriptions.Dispose();
}
public static VariableContainer Initialize(IPlc plc, IReadOnlyList<string> 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; }
}

View File

@@ -5,13 +5,14 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>s7mon</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.49.0" /> <PackageReference Include="Spectre.Console" Version="0.49.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.0" /> <PackageReference Include="Spectre.Console.Cli" Version="0.49.0" />
<PackageReference Include="Sharp7.Rx" Version="2.0.8-prerelease" /> <PackageReference Include="Sharp7.Rx" Version="2.0.9-prerelease" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All"/> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>