Implement basic funtionality
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
205
Sharp7.Monitor/ReadPlcCommand.cs
Normal file
205
Sharp7.Monitor/ReadPlcCommand.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user