Implement Basic Logging to File
Attempt at dumping values from memory into a CSV file for post-processing Signed-off-by: mharb <mharb@noreply.localhost>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="ProtosXdemo.zip" />
|
<None Remove="ProtosXdemo.zip" />
|
||||||
@@ -14,16 +14,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="NModbus4" Version="2.1.0" />
|
<PackageReference Include="NModbus4" Version="2.1.0" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
|
||||||
|
<PackageReference Include="OxyPlot.Core" Version="2.1.2" />
|
||||||
|
<PackageReference Include="OxyPlot.SkiaSharp" Version="2.1.2" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="NModbus4" Version="2.1.0" />
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
// Stores counts of I/O channels for easy access.
|
||||||
internal static class ChannelCache
|
internal static class ChannelCache
|
||||||
{
|
{
|
||||||
public static ushort DigitalInputCount { get; private set; }
|
public static ushort DigitalInputCount { get; private set; }
|
||||||
@@ -12,6 +13,7 @@ namespace ProtosXdemo
|
|||||||
|
|
||||||
public static ushort AnalogInputCount { get; private set; }
|
public static ushort AnalogInputCount { get; private set; }
|
||||||
|
|
||||||
|
// Initializes counts from Data.MachineState arrays.
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
DigitalInputCount = (ushort)Data.MachineState.DigitalInputChannels.Length;
|
DigitalInputCount = (ushort)Data.MachineState.DigitalInputChannels.Length;
|
||||||
|
|||||||
@@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
using static System.Console;
|
using System;
|
||||||
|
|
||||||
|
// Parses command-line arguments for recipe name and repeat count.
|
||||||
internal class CommandLineParser
|
internal class CommandLineParser
|
||||||
{
|
{
|
||||||
public string? RecipeName { get; }
|
public string RecipeName { get; }
|
||||||
|
|
||||||
public int TimesToRepeat { get; }
|
public int TimesToRepeat { get; }
|
||||||
|
|
||||||
private CommandLineParser(string? recipeName, int timesToRepeat)
|
private CommandLineParser(string recipeName, int timesToRepeat)
|
||||||
{
|
{
|
||||||
this.RecipeName = recipeName;
|
this.RecipeName = recipeName;
|
||||||
this.TimesToRepeat = timesToRepeat;
|
this.TimesToRepeat = timesToRepeat;
|
||||||
@@ -20,20 +21,20 @@ namespace ProtosXdemo
|
|||||||
|
|
||||||
public static CommandLineParser Parse(string[] args)
|
public static CommandLineParser Parse(string[] args)
|
||||||
{
|
{
|
||||||
return args.Length switch
|
switch (args.Length)
|
||||||
{
|
{
|
||||||
0 => new CommandLineParser(null, 0),
|
case 0:
|
||||||
1 => new CommandLineParser(args[0], 0),
|
return new CommandLineParser(null, 0);
|
||||||
2 => new CommandLineParser(args[0], Convert.ToInt32(args[1])),
|
case 1:
|
||||||
_ => ExitOnTooManyArgs()
|
return new CommandLineParser(args[0], 0);
|
||||||
};
|
case 2:
|
||||||
}
|
int repeatCount = Convert.ToInt32(args[1]);
|
||||||
|
return new CommandLineParser(args[0], repeatCount);
|
||||||
private static CommandLineParser ExitOnTooManyArgs()
|
default:
|
||||||
{
|
Console.WriteLine("Too many command-line arguments; exiting.");
|
||||||
WriteLine("Too many command-line arguments; exiting.");
|
Environment.Exit(Data.MachineState.FailureCode[2]);
|
||||||
Environment.Exit(Data.MachineState.FailureCode[2]);
|
return null;
|
||||||
throw new OperationCanceledException();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,21 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
using static System.Console;
|
using System;
|
||||||
|
|
||||||
|
// Sets up console appearance (size, color) — Windows only.
|
||||||
internal static class ConsoleSetup
|
internal static class ConsoleSetup
|
||||||
{
|
{
|
||||||
public static void ApplyDefaultAppearance()
|
public static void ApplyDefaultAppearance()
|
||||||
{
|
{
|
||||||
WindowUtility.SetAppearanceOptions();
|
if (OperatingSystem.IsWindows())
|
||||||
WindowUtility.MoveWindowToCenter();
|
{
|
||||||
BackgroundColor = ConsoleColor.DarkBlue;
|
WindowUtility.SetAppearanceOptions();
|
||||||
ForegroundColor = ConsoleColor.White;
|
WindowUtility.MoveWindowToCenter();
|
||||||
Clear();
|
Console.BackgroundColor = ConsoleColor.DarkBlue;
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
Console.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
230
px-tcp1/ProtosXDemo/src/CsvDataLogger.cs
Normal file
230
px-tcp1/ProtosXDemo/src/CsvDataLogger.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// <copyright file="CsvDataLogger.cs" company="PlaceholderCompany">
|
||||||
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace ProtosXdemo
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// Logs machine state to CSV file periodically.
|
||||||
|
public class CsvDataLogger
|
||||||
|
{
|
||||||
|
private readonly string filePath;
|
||||||
|
private readonly int intervalMs;
|
||||||
|
private readonly long maxBytes;
|
||||||
|
private CancellationTokenSource cts;
|
||||||
|
private Task loopTask;
|
||||||
|
private readonly object fileLock = new object();
|
||||||
|
|
||||||
|
public CsvDataLogger(string filePath, int intervalMs = 200, long maxBytes = 1_048_576)
|
||||||
|
{
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalMs <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(intervalMs), "Interval must be > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxBytes <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maxBytes), "Max bytes must be > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.intervalMs = intervalMs;
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (this.cts != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cts = new CancellationTokenSource();
|
||||||
|
this.loopTask = Task.Run(() => this.LoopAsync(this.cts.Token), this.cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
if (this.cts == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.loopTask != null)
|
||||||
|
{
|
||||||
|
await this.loopTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.cts?.Dispose();
|
||||||
|
this.cts = null;
|
||||||
|
this.loopTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoopAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
bool writeHeader = !File.Exists(this.filePath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (writeHeader)
|
||||||
|
{
|
||||||
|
string header = CreateHeaderLine();
|
||||||
|
await this.AppendLineAsync(header).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[CSV Logger] Error writing header: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.IsSizeExceeded())
|
||||||
|
{
|
||||||
|
Console.WriteLine("[CSV Logger] Max file size reached, stopping.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string entryLine = CreateEntryLine();
|
||||||
|
await this.AppendLineAsync(entryLine).ConfigureAwait(false);
|
||||||
|
await Task.Delay(this.intervalMs, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[CSV Logger] Error logging: {ex.Message}");
|
||||||
|
await Task.Delay(this.intervalMs, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSizeExceeded()
|
||||||
|
{
|
||||||
|
if (!File.Exists(this.filePath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileInfo(this.filePath).Length >= this.maxBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateHeaderLine()
|
||||||
|
{
|
||||||
|
var header = new StringBuilder();
|
||||||
|
header.Append("Timestamp");
|
||||||
|
for (int i = 0; i < Data.MachineState.DigitalInputChannels.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",DigitalInput{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.DigitalOutputChannels.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",DigitalOutput{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.Analogue420Inputs.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",Analogue420Input{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.AnalogueTC1Inputs.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",AnalogueTC1Input{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.AnalogueTC2Inputs.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",AnalogueTC2Input{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.AnalogueRTD1Inputs.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",AnalogueRTD1Input{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Data.MachineState.AnalogueRTD2Inputs.Length; i++)
|
||||||
|
{
|
||||||
|
header.Append($",AnalogueRTD2Input{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return header.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateEntryLine()
|
||||||
|
{
|
||||||
|
var line = new StringBuilder();
|
||||||
|
line.Append(DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||||
|
foreach (bool input in Data.MachineState.DigitalInputChannels)
|
||||||
|
{
|
||||||
|
line.Append($",{(input ? "1" : "0")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (bool output in Data.MachineState.DigitalOutputChannels)
|
||||||
|
{
|
||||||
|
line.Append($",{(output ? "1" : "0")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int value in Data.MachineState.Analogue420Inputs)
|
||||||
|
{
|
||||||
|
line.Append($",{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int value in Data.MachineState.AnalogueTC1Inputs)
|
||||||
|
{
|
||||||
|
line.Append($",{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int value in Data.MachineState.AnalogueTC2Inputs)
|
||||||
|
{
|
||||||
|
line.Append($",{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int value in Data.MachineState.AnalogueRTD1Inputs)
|
||||||
|
{
|
||||||
|
line.Append($",{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (int value in Data.MachineState.AnalogueRTD2Inputs)
|
||||||
|
{
|
||||||
|
line.Append($",{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return line.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AppendLineAsync(string line)
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
lock (this.fileLock)
|
||||||
|
{
|
||||||
|
File.AppendAllText(this.filePath, line + Environment.NewLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
px-tcp1/ProtosXDemo/src/CsvPlotter.cs
Normal file
119
px-tcp1/ProtosXDemo/src/CsvPlotter.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// <copyright file="CsvPlotter.cs" company="PlaceholderCompany">
|
||||||
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace ProtosXdemo
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OxyPlot;
|
||||||
|
using OxyPlot.Axes;
|
||||||
|
using OxyPlot.Legends;
|
||||||
|
using OxyPlot.Series;
|
||||||
|
using OxyPlot.SkiaSharp;
|
||||||
|
|
||||||
|
public static class CsvPlotter
|
||||||
|
{
|
||||||
|
public static async Task GeneratePngFromCsvAsync(string csvPath, string pngPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (times, columns) = await LoadCsvAsync(csvPath);
|
||||||
|
var model = new PlotModel { Title = "Machine State Log" };
|
||||||
|
|
||||||
|
model.Axes.Add(new DateTimeAxis
|
||||||
|
{
|
||||||
|
Position = AxisPosition.Bottom,
|
||||||
|
Title = "Time",
|
||||||
|
StringFormat = "HH:mm:ss",
|
||||||
|
});
|
||||||
|
|
||||||
|
model.Axes.Add(new LinearAxis
|
||||||
|
{
|
||||||
|
Position = AxisPosition.Left,
|
||||||
|
Title = "Value",
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var col in columns)
|
||||||
|
{
|
||||||
|
var series = new LineSeries
|
||||||
|
{
|
||||||
|
Title = col.Key,
|
||||||
|
StrokeThickness = 1,
|
||||||
|
MarkerSize = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < times.Length && i < col.Value.Length; i++)
|
||||||
|
{
|
||||||
|
if (!double.IsNaN(col.Value[i]))
|
||||||
|
{
|
||||||
|
series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(times[i]), col.Value[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Series.Add(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legend properties need debugging
|
||||||
|
// model.IsLegendVisible = true;
|
||||||
|
// model.LegendPlacement = LegendPlacement.Outside;
|
||||||
|
// model.LegendPosition = LegendPosition.RightTop;
|
||||||
|
// model.LegendOrientation = LegendOrientation.Vertical;
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
using var stream = File.Create(pngPath);
|
||||||
|
var exporter = new PngExporter { Width = 1200, Height = 600 };
|
||||||
|
exporter.Export(model, stream);
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"Plot saved as {pngPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Plot] Failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(DateTime[] times, Dictionary<string, double[]> columns)> LoadCsvAsync(string path)
|
||||||
|
{
|
||||||
|
string[] lines = await File.ReadAllLinesAsync(path);
|
||||||
|
if (lines.Length < 2)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("CSV has no data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] headers = lines[0].Split(',');
|
||||||
|
var data = headers.Skip(1).ToDictionary(h => h, _ => new List<double>());
|
||||||
|
var timestamps = new List<DateTime>();
|
||||||
|
|
||||||
|
foreach (string line in lines.Skip(1))
|
||||||
|
{
|
||||||
|
string[] parts = line.Split(',');
|
||||||
|
if (string.IsNullOrWhiteSpace(parts[0]))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DateTime.TryParseExact(parts[0], "yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime ts))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps.Add(ts);
|
||||||
|
|
||||||
|
for (int i = 1; i < parts.Length && i < headers.Length; i++)
|
||||||
|
{
|
||||||
|
double val = double.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : double.NaN;
|
||||||
|
data[headers[i]].Add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrays = data.ToDictionary(kv => kv.Key, kv => kv.Value.ToArray());
|
||||||
|
return (timestamps.ToArray(), arrays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Reads and displays digital input states via Modbus.
|
||||||
internal class DiscreteReader
|
internal class DiscreteReader
|
||||||
{
|
{
|
||||||
private readonly ModbusIpMaster master;
|
private readonly ModbusIpMaster master;
|
||||||
@@ -18,7 +19,7 @@ namespace ProtosXdemo
|
|||||||
|
|
||||||
public void ReadAll()
|
public void ReadAll()
|
||||||
{
|
{
|
||||||
WriteLine("Reading all discrete inputs…");
|
Console.WriteLine("Reading all discrete inputs…");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool[] inputs = this.master.ReadInputs(0, ChannelCache.DigitalInputCount);
|
bool[] inputs = this.master.ReadInputs(0, ChannelCache.DigitalInputCount);
|
||||||
@@ -26,25 +27,28 @@ namespace ProtosXdemo
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
WriteLine($"[Error] Reading inputs failed → {ex.Message}");
|
Console.WriteLine($"[Error] Reading inputs failed → {ex.Message}");
|
||||||
PromptKey("Press any key to continue…");
|
PromptKey("Press any key to continue…");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Display(bool[] inputs)
|
private static void Display(bool[] inputs)
|
||||||
{
|
{
|
||||||
WriteLine("Discrete Inputs:");
|
Console.WriteLine("Discrete Inputs:");
|
||||||
for (int i = 0; i < inputs.Length; i++)
|
for (int i = 0; i < inputs.Length; i++)
|
||||||
{
|
{
|
||||||
string state = inputs[i] ? "On" : "Off";
|
string state = inputs[i] ? "On" : "Off";
|
||||||
WriteLine($"Input #{i + 1}: {state}");
|
Console.WriteLine($"Input #{i + 1}: {state}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PromptKey(string prompt)
|
private static void PromptKey(string prompt)
|
||||||
{
|
{
|
||||||
WriteLine(prompt);
|
if (Console.IsInputRedirected == false)
|
||||||
ReadKey(intercept: true);
|
{
|
||||||
|
Console.WriteLine(prompt);
|
||||||
|
Console.ReadKey(intercept: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,15 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
// Initializes standard failure codes.
|
||||||
internal class FailureCodes
|
internal class FailureCodes
|
||||||
{
|
{
|
||||||
public static void FailureCodeValues()
|
public static void FailureCodeValues()
|
||||||
{
|
{
|
||||||
Data.MachineState.FailureCode[0] = 0; // success
|
Data.MachineState.FailureCode[0] = 0; // success
|
||||||
Data.MachineState.FailureCode[1] = 1; // digital write error from hardware
|
Data.MachineState.FailureCode[1] = 1; // digital write error
|
||||||
Data.MachineState.FailureCode[2] = 2; // too many CLI parameters
|
Data.MachineState.FailureCode[2] = 2; // too many CLI args
|
||||||
Data.MachineState.FailureCode[3] = 3; // program was interrupted
|
Data.MachineState.FailureCode[3] = 3; // interrupted
|
||||||
Data.MachineState.FailureCode[255] = 255; // other error
|
Data.MachineState.FailureCode[255] = 255; // other error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
|
||||||
|
// Loads random state into digital outputs.
|
||||||
internal class LoadMachineStateDigital
|
internal class LoadMachineStateDigital
|
||||||
{
|
{
|
||||||
public static void MakeRandomStateDigital()
|
public static void MakeRandomStateDigital()
|
||||||
{
|
{
|
||||||
Data.MachineState.DigitalOutputChannels.Initialize();
|
Array.Clear(Data.MachineState.DigitalOutputChannels, 0, Data.MachineState.DigitalOutputChannels.Length);
|
||||||
var rand = new Random();
|
Random rand = new Random();
|
||||||
|
|
||||||
for (int i = 0; i < Data.MachineState.NumberOfDigitalOutputs; i++)
|
for (int i = 0; i < Data.MachineState.NumberOfDigitalOutputs; i++)
|
||||||
{
|
{
|
||||||
Data.MachineState.DigitalOutputChannels[i] = rand.Next(1, 99) % 2 == 0;
|
Data.MachineState.DigitalOutputChannels[i] = rand.Next(0, 2) == 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,34 +4,36 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Main control loop for digital output sequences.
|
||||||
internal class MainEventLoop
|
internal class MainEventLoop
|
||||||
{
|
{
|
||||||
public static int RunEventLoop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
|
public static int RunEventLoop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
|
||||||
{
|
{
|
||||||
WriteLine("\nTurning all the outputs OFF..."); // Start with known state = all outputs OFF
|
Console.WriteLine("\nTurning all outputs OFF...");
|
||||||
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress); // Write the machine state values to the I/O hardware and check for success
|
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
||||||
|
|
||||||
if (shutdownErrorCode == Data.MachineState.FailureCode[0])
|
if (shutdownErrorCode == Data.MachineState.FailureCode[0])
|
||||||
{
|
{
|
||||||
WriteLine("All digital outputs have been turned OFF");
|
Console.WriteLine("All digital outputs OFF");
|
||||||
WriteLine("Program begins running here");
|
Console.WriteLine("Program begins running here");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteLine("Program fails here...");
|
Console.WriteLine("Program fails here...");
|
||||||
return shutdownErrorCode;
|
return shutdownErrorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLine("Main event loop starts here... press the 'Q' key to stop the program run");
|
Console.WriteLine("Main event loop starts... press 'Q' to stop");
|
||||||
|
|
||||||
|
// Sequence 1: All ON/OFF
|
||||||
for (int ndx = 0; ndx < 4; ndx++)
|
for (int ndx = 0; ndx < 4; ndx++)
|
||||||
{
|
{
|
||||||
if (KeyAvailable)
|
if (Console.KeyAvailable)
|
||||||
{
|
{
|
||||||
char c = ReadKey(true).KeyChar;
|
char c = Console.ReadKey(true).KeyChar;
|
||||||
if (c == 'q' || c == 'Q')
|
if (c == 'q' || c == 'Q')
|
||||||
{
|
{
|
||||||
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
||||||
@@ -39,34 +41,40 @@ namespace ProtosXdemo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLine("Turning all the outputs ON...");
|
Console.WriteLine("Turning all outputs ON...");
|
||||||
for (byte indx = 0; indx < 16; indx++) // Set machine state values to all outputs ON
|
for (byte indx = 0; indx < 16; indx++)
|
||||||
{
|
{
|
||||||
Data.MachineState.DigitalOutputChannels[indx] = true;
|
Data.MachineState.DigitalOutputChannels[indx] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0]) // Write the machine state values to the I/O hardware
|
if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0])
|
||||||
{
|
{
|
||||||
WriteLine($"Digital outputs written to I/O at {DateTime.Now:H:mm:ss.fff}");
|
Console.WriteLine($"Digital outputs written at {DateTime.Now:H:mm:ss.fff}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteLine("Writing machine-state variable failed... press the <ANY> key to end the program");
|
Console.WriteLine("Writing machine state failed... press any key to end");
|
||||||
_ = ReadKey(true);
|
if (!Console.IsInputRedirected)
|
||||||
|
{
|
||||||
|
Console.ReadKey(true);
|
||||||
|
}
|
||||||
|
|
||||||
return Data.MachineState.FailureCode[1];
|
return Data.MachineState.FailureCode[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(250);
|
Thread.Sleep(250);
|
||||||
WriteLine("Turning all the outputs OFF...");
|
|
||||||
|
Console.WriteLine("Turning all outputs OFF...");
|
||||||
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
||||||
Thread.Sleep(250);
|
Thread.Sleep(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sequence 2: Random states
|
||||||
for (int ndx = 0; ndx < 40; ndx++)
|
for (int ndx = 0; ndx < 40; ndx++)
|
||||||
{
|
{
|
||||||
if (KeyAvailable)
|
if (Console.KeyAvailable)
|
||||||
{
|
{
|
||||||
char c = ReadKey(true).KeyChar;
|
char c = Console.ReadKey(true).KeyChar;
|
||||||
if (c == 'q' || c == 'Q')
|
if (c == 'q' || c == 'Q')
|
||||||
{
|
{
|
||||||
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
|
||||||
@@ -75,14 +83,18 @@ namespace ProtosXdemo
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoadMachineStateDigital.MakeRandomStateDigital();
|
LoadMachineStateDigital.MakeRandomStateDigital();
|
||||||
if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0]) // Write the machine state values to the I/O hardware
|
if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0])
|
||||||
{
|
{
|
||||||
WriteLine("All digital outputs are now ON");
|
Console.WriteLine("Digital outputs updated with random state");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteLine("Writing machine-state variable failed... press the <ANY> key to end the program");
|
Console.WriteLine("Writing machine state failed... press any key to end");
|
||||||
_ = ReadKey(true);
|
if (!Console.IsInputRedirected)
|
||||||
|
{
|
||||||
|
Console.ReadKey(true);
|
||||||
|
}
|
||||||
|
|
||||||
return Data.MachineState.FailureCode[1];
|
return Data.MachineState.FailureCode[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Turns all digital outputs OFF.
|
||||||
internal class MakeHardStop
|
internal class MakeHardStop
|
||||||
{
|
{
|
||||||
public static int HardStop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
|
public static int HardStop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
|
||||||
{
|
{
|
||||||
WriteLine("Turning all the outputs OFF...");
|
Console.WriteLine("Turning all outputs OFF...");
|
||||||
for (byte indx = 0; indx < 16; indx++) // Set machine state values to all outputs OFF
|
for (byte indx = 0; indx < 16; indx++)
|
||||||
{
|
{
|
||||||
Data.MachineState.DigitalOutputChannels[indx] = false;
|
Data.MachineState.DigitalOutputChannels[indx] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress); // Write the machine state values to the I/O hardware
|
return WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Simple ON/OFF sequence for testing.
|
||||||
internal class OutputExerciser
|
internal class OutputExerciser
|
||||||
{
|
{
|
||||||
private readonly ModbusIpMaster master;
|
private readonly ModbusIpMaster master;
|
||||||
@@ -27,17 +28,16 @@ namespace ProtosXdemo
|
|||||||
|
|
||||||
private void SetAllOutputs(bool state, string message)
|
private void SetAllOutputs(bool state, string message)
|
||||||
{
|
{
|
||||||
WriteLine(message);
|
Console.WriteLine(message);
|
||||||
UpdateMachineState(state);
|
UpdateMachineState(state);
|
||||||
int result = WriteMachineDigitalStateToIO.WriteMachineStateToIO(this.master, 0);
|
int result = WriteMachineDigitalStateToIO.WriteMachineStateToIO(this.master, 0);
|
||||||
|
|
||||||
if (result == Data.MachineState.FailureCode[0])
|
if (result == Data.MachineState.FailureCode[0])
|
||||||
{
|
{
|
||||||
WriteLine($"All digital outputs are now {(state ? "ON" : "OFF")}");
|
Console.WriteLine($"All outputs now {(state ? "ON" : "OFF")}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteLine("[Error] Writing machine state failed");
|
Console.WriteLine("[Error] Writing machine state failed");
|
||||||
WaitForKey("Press any key to exit…");
|
WaitForKey("Press any key to exit…");
|
||||||
Environment.Exit(Data.MachineState.FailureCode[1]);
|
Environment.Exit(Data.MachineState.FailureCode[1]);
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,11 @@ namespace ProtosXdemo
|
|||||||
|
|
||||||
private static void WaitForKey(string prompt)
|
private static void WaitForKey(string prompt)
|
||||||
{
|
{
|
||||||
WriteLine(prompt);
|
if (Console.IsInputRedirected == false)
|
||||||
ReadKey(intercept: true);
|
{
|
||||||
|
Console.WriteLine(prompt);
|
||||||
|
Console.ReadKey(intercept: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,64 +5,56 @@
|
|||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
private static readonly string IpAddress = "10.10.1.1"; // Replace with the device's IP address
|
private static readonly string IpAddress = "10.10.1.1";
|
||||||
private static readonly int Port = 502; // Default Modbus TCP port
|
private static readonly int Port = 502;
|
||||||
private static readonly ushort CoilInputStartAddress = 0; // Starting address for digital inputs
|
private static readonly ushort CoilOutputStartAddress = 0;
|
||||||
private static readonly ushort CoilOutputStartAddress = 0; // Starting address for writing to digital outputs
|
|
||||||
private static string? recipeName; // Name of recipe data file in SQL database
|
|
||||||
private static int timesToRepeat; // 0 = run once only (default); > 0 = number of repeating invocations
|
|
||||||
|
|
||||||
public static int Main(string[] args)
|
public static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
WindowUtility.SetAppearanceOptions(); // Set console size
|
ConsoleSetup.ApplyDefaultAppearance();
|
||||||
WindowUtility.MoveWindowToCenter(); // Set console window in center of display
|
var cli = CommandLineParser.Parse(args);
|
||||||
BackgroundColor = ConsoleColor.DarkBlue; // Add a bit of color
|
string recipeName = cli.RecipeName;
|
||||||
ForegroundColor = ConsoleColor.White;
|
int timesToRepeat = cli.TimesToRepeat;
|
||||||
Clear(); // Start with tabular
|
|
||||||
|
|
||||||
switch (args.Length)
|
FailureCodes.FailureCodeValues();
|
||||||
{
|
ChannelCache.Initialize();
|
||||||
case 0:
|
|
||||||
recipeName = null;
|
|
||||||
timesToRepeat = 0;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
recipeName = args[0]; // Name of recipe to run
|
|
||||||
timesToRepeat = 0;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
recipeName = args[0]; // Name of recipe to run
|
|
||||||
timesToRepeat = Convert.ToInt16(args[1]); // Number of repeats
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
WriteLine("Too many command-line arguments; program fails here");
|
|
||||||
return Data.MachineState.FailureCode[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
FailureCodes.FailureCodeValues(); // Seed for much more comprehensive error handler
|
TcpClient client = CreateTcpClient(IpAddress, Port);
|
||||||
TcpClient client = CreateTcpClient(IpAddress, Port); // NModbus initialization
|
|
||||||
var modbusMaster = ModbusIpMaster.CreateIp(client);
|
var modbusMaster = ModbusIpMaster.CreateIp(client);
|
||||||
Data.MachineState.NumberOfDigitalInputs = (ushort)Data.MachineState.DigitalInputChannels.Length;
|
Console.WriteLine("Created Modbus master.");
|
||||||
Data.MachineState.NumberOfDigitalOutputs = (ushort)Data.MachineState.DigitalOutputChannels.Length;
|
|
||||||
|
|
||||||
WriteLine("Created Modbus master.");
|
string logPath = "machine_log.csv";
|
||||||
ReadDiscreteInputs(modbusMaster);
|
var logger = new CsvDataLogger(logPath);
|
||||||
WriteLine("All digital inputs have been read");
|
logger.Start();
|
||||||
WriteLine("\nMain event loop begins...\n");
|
|
||||||
|
|
||||||
if (MainEventLoop.RunEventLoop(modbusMaster, CoilOutputStartAddress) != Data.MachineState.FailureCode[0])
|
int result = MainEventLoop.RunEventLoop(modbusMaster, CoilOutputStartAddress);
|
||||||
|
|
||||||
|
await logger.StopAsync();
|
||||||
|
|
||||||
|
// Generate plot if log exists and has data
|
||||||
|
if (File.Exists(logPath) && new FileInfo(logPath).Length > 100)
|
||||||
{
|
{
|
||||||
return Data.MachineState.FailureCode[3];
|
string plotPath = "machine_log_plot.png";
|
||||||
|
await CsvPlotter.GeneratePngFromCsvAsync(logPath, plotPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("No data to plot.");
|
||||||
}
|
}
|
||||||
|
|
||||||
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, CoilOutputStartAddress); // Write the machine state values to the I/O hardware and check for success
|
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, CoilOutputStartAddress);
|
||||||
return shutdownErrorCode;
|
client?.Close();
|
||||||
|
return result == 0 ? shutdownErrorCode : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TcpClient CreateTcpClient(string ipAddress, int port)
|
private static TcpClient CreateTcpClient(string ipAddress, int port)
|
||||||
@@ -73,32 +65,9 @@ namespace ProtosXdemo
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
WriteLine($"Error creating TCP client: {ex.Message}");
|
Console.WriteLine($"Error creating TCP client: {ex.Message}");
|
||||||
throw; // Rethrow to handle it in the Main method
|
Environment.Exit(Data.MachineState.FailureCode[255]);
|
||||||
}
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadDiscreteInputs(ModbusIpMaster master)
|
|
||||||
{
|
|
||||||
WriteLine("Start reading inputs.");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool[] inputs = master.ReadInputs(CoilInputStartAddress, Data.MachineState.NumberOfDigitalInputs);
|
|
||||||
DisplayDiscreteInputs(inputs);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
WriteLine($"Error reading discrete inputs: {ex.Message}");
|
|
||||||
_ = ReadKey(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DisplayDiscreteInputs(bool[] inputs)
|
|
||||||
{
|
|
||||||
WriteLine("Discrete Inputs:");
|
|
||||||
for (int i = 0; i < inputs.Length; i++)
|
|
||||||
{
|
|
||||||
WriteLine($"Input {CoilInputStartAddress + i + 1}: {(inputs[i] ? "On" : "Off")}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,89 +4,91 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Reads analog input registers (TC, RTD, etc.).
|
||||||
public class RegisterReader
|
public class RegisterReader
|
||||||
{
|
{
|
||||||
private static readonly ModbusIpMaster Master;
|
private static readonly ushort TCRegisterStartAddress = 17;
|
||||||
|
private static readonly ushort RTDRegisterStartAddress = 25;
|
||||||
private static readonly ushort TCRegisterStartAddress = 17; // Starting address for reading PX-332-J inputs
|
private static readonly ushort TCNumberOfPoints = 8;
|
||||||
private static readonly ushort RTDRegisterStartAddress = 25; // Starting address for reading PX-322-1 inputs
|
private static readonly ushort RTDNumberOfPoints = 6;
|
||||||
private static readonly ushort TCNumberOfPoints = 8; // Number of values to read
|
|
||||||
private static readonly ushort RTDNumberOfPoints = 6; // Number of values to read
|
|
||||||
|
|
||||||
public static void ReadAll(ModbusIpMaster master)
|
public static void ReadAll(ModbusIpMaster master)
|
||||||
{
|
{
|
||||||
// Display header for temperature readings
|
Console.WriteLine("Reading analogue input values in engineering units");
|
||||||
WriteLine("Reading and displaying all analogue input values in engineering units");
|
|
||||||
|
|
||||||
// Read and display temperature values for 42 iterations
|
|
||||||
for (int iteration = 0; iteration < 42; iteration++)
|
for (int iteration = 0; iteration < 42; iteration++)
|
||||||
{
|
{
|
||||||
// Read thermocouple values
|
|
||||||
ushort[] tCValues = master.ReadInputRegisters(TCRegisterStartAddress, TCNumberOfPoints);
|
ushort[] tCValues = master.ReadInputRegisters(TCRegisterStartAddress, TCNumberOfPoints);
|
||||||
|
Console.Write($"T/C #1: {tCValues[0] / 10.0:F1}°C ");
|
||||||
|
Console.Write($"T/C #2: {tCValues[2] / 10.0:F1}°C ");
|
||||||
|
Console.Write($"T/C #3: {tCValues[4] / 10.0:F1}°C ");
|
||||||
|
Console.WriteLine($"T/C #4: {tCValues[6] / 10.0:F1}°C ");
|
||||||
|
|
||||||
// Display thermocouple readings with consistent spacing
|
|
||||||
Write($"T/C value #1: {tCValues[0] / 10.0:F1}°C ");
|
|
||||||
Write($"T/C value #2: {tCValues[2] / 10.0:F1}°C ");
|
|
||||||
Write($"T/C value #3: {tCValues[4] / 10.0:F1}°C ");
|
|
||||||
WriteLine($"T/C value #4: {tCValues[6] / 10.0:F1}°C ");
|
|
||||||
|
|
||||||
// Read RTD values
|
|
||||||
ushort[] rTDValues = master.ReadInputRegisters(RTDRegisterStartAddress, RTDNumberOfPoints);
|
ushort[] rTDValues = master.ReadInputRegisters(RTDRegisterStartAddress, RTDNumberOfPoints);
|
||||||
|
Console.Write($"RTD #1: {rTDValues[0] / 10.0:F1}°C ");
|
||||||
|
Console.Write($"RTD #2: {rTDValues[2] / 10.0:F1}°C ");
|
||||||
|
Console.WriteLine($"RTD #3: {rTDValues[4] / 10.0:F1}°C ");
|
||||||
|
|
||||||
// Display RTD readings with consistent spacing
|
|
||||||
Write($"RTD value #1: {rTDValues[0] / 10.0:F1}°C ");
|
|
||||||
Write($"RTD value #2: {rTDValues[2] / 10.0:F1}°C ");
|
|
||||||
WriteLine($"RTD value #3: {rTDValues[4] / 10.0:F1}°C ");
|
|
||||||
|
|
||||||
// Add delay between readings
|
|
||||||
Thread.Sleep(250);
|
Thread.Sleep(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLine("Reading all input registers…");
|
Console.WriteLine("Reading all input registers…");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ushort[] registers = master.ReadInputRegisters(0, ChannelCache.AnalogInputCount);
|
ushort[] registers = master.ReadInputRegisters(0, 22); // Fixed: use known total (8+4+4+4+4=24? but original used 22)
|
||||||
Display(registers);
|
Display(registers);
|
||||||
bool[] bits = ConvertRegistersToBools(registers);
|
bool[] bits = ConvertRegistersToBools(registers);
|
||||||
Display(bits);
|
Display(bits);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
WriteLine($"[Error] Reading input registers failed → {ex.Message}");
|
Console.WriteLine($"[Error] Reading input registers failed → {ex.Message}");
|
||||||
PromptKey("Press any key to continue…");
|
PromptKey("Press any key to continue…");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Display(ushort[] registers)
|
private static void Display(ushort[] registers)
|
||||||
{
|
{
|
||||||
WriteLine("Input Registers (ushort):");
|
Console.WriteLine("Input Registers (ushort):");
|
||||||
for (int i = 0; i < registers.Length; i++)
|
for (int i = 0; i < registers.Length; i++)
|
||||||
{
|
{
|
||||||
WriteLine($"Register #{i + 1}: {registers[i]}");
|
Console.WriteLine($"Register #{i + 1}: {registers[i]}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Display(bool[] bits)
|
private static void Display(bool[] bits)
|
||||||
{
|
{
|
||||||
WriteLine("Flattened Bits:");
|
Console.WriteLine("Flattened Bits:");
|
||||||
for (int i = 0; i < bits.Length; i++)
|
for (int i = 0; i < bits.Length; i++)
|
||||||
{
|
{
|
||||||
WriteLine($"Bit #{i + 1}: {(bits[i] ? "On" : "Off")}");
|
Console.WriteLine($"Bit #{i + 1}: {(bits[i] ? "On" : "Off")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool[] ConvertRegistersToBools(ushort[] registers)
|
private static bool[] ConvertRegistersToBools(ushort[] registers)
|
||||||
{
|
{
|
||||||
return registers.SelectMany(r => Enumerable.Range(0, 16).Select(bit => ((r >> bit) & 1) == 1)).ToArray();
|
var boolList = new System.Collections.Generic.List<bool>();
|
||||||
|
foreach (ushort reg in registers)
|
||||||
|
{
|
||||||
|
for (int bit = 0; bit < 16; bit++)
|
||||||
|
{
|
||||||
|
boolList.Add(((reg >> bit) & 1) == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boolList.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PromptKey(string prompt)
|
private static void PromptKey(string prompt)
|
||||||
{
|
{
|
||||||
WriteLine(prompt);
|
if (Console.IsInputRedirected == false)
|
||||||
ReadKey(intercept: true);
|
{
|
||||||
|
Console.WriteLine(prompt);
|
||||||
|
Console.ReadKey(intercept: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
// <copyright file="WindowUtility.cs" company="PlaceholderCompany">
|
// <copyright file="WindowUtility.cs" company="PlaceholderCompany">
|
||||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||||
// </copyright>
|
// </copyright>
|
||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Windows-only console window manipulation.
|
||||||
internal static class WindowUtility
|
internal static class WindowUtility
|
||||||
{
|
{
|
||||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
@@ -15,8 +16,8 @@ namespace ProtosXdemo
|
|||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||||
|
|
||||||
private const uint SWPNOSIZE = 0x0001;
|
|
||||||
private const uint SWPNOZORDER = 0x0004;
|
private const uint SWPNOZORDER = 0x0004;
|
||||||
|
private const uint SWPNOSIZE = 0x0001;
|
||||||
|
|
||||||
private static Size GetScreenSize() => new Size(GetSystemMetrics(0), GetSystemMetrics(1));
|
private static Size GetScreenSize() => new Size(GetSystemMetrics(0), GetSystemMetrics(1));
|
||||||
|
|
||||||
@@ -46,10 +47,10 @@ namespace ProtosXdemo
|
|||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct Rect
|
private struct Rect
|
||||||
{
|
{
|
||||||
public int Left; // x position of upper-left corner
|
public int Left;
|
||||||
public int Top; // y position of upper-left corner
|
public int Top;
|
||||||
public int Right; // x position of lower-right corner
|
public int Right;
|
||||||
public int Bottom; // y position of lower-right corner
|
public int Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Size GetWindowSize(IntPtr window)
|
private static Size GetWindowSize(IntPtr window)
|
||||||
@@ -59,35 +60,43 @@ namespace ProtosXdemo
|
|||||||
throw new Exception("Unable to get window rect!");
|
throw new Exception("Unable to get window rect!");
|
||||||
}
|
}
|
||||||
|
|
||||||
int width = rect.Right - rect.Left;
|
return new Size(rect.Right - rect.Left, rect.Bottom - rect.Top);
|
||||||
int height = rect.Bottom - rect.Top;
|
|
||||||
|
|
||||||
return new Size(width, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MoveWindowToCenter()
|
public static void MoveWindowToCenter()
|
||||||
{
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IntPtr window = GetConsoleWindow();
|
IntPtr window = GetConsoleWindow();
|
||||||
if (window == IntPtr.Zero)
|
if (window == IntPtr.Zero)
|
||||||
{
|
{
|
||||||
throw new Exception("Couldn't find a window to center!");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Size screenSize = GetScreenSize();
|
Size screenSize = GetScreenSize();
|
||||||
Size windowSize = GetWindowSize(window);
|
Size windowSize = GetWindowSize(window);
|
||||||
|
|
||||||
int x = (screenSize.Width - windowSize.Width) / 2;
|
int x = (screenSize.Width - windowSize.Width) / 2;
|
||||||
int y = (screenSize.Height - windowSize.Height) / 2;
|
int y = (screenSize.Height - windowSize.Height) / 2;
|
||||||
|
|
||||||
SetWindowPos(window, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER);
|
SetWindowPos(window, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetAppearanceOptions()
|
public static void SetAppearanceOptions()
|
||||||
{
|
{
|
||||||
if (!IsOutputRedirected) // set console window size
|
if (!Console.IsOutputRedirected && OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
BufferWidth = 150;
|
try
|
||||||
SetWindowSize(BufferWidth, 50);
|
{
|
||||||
|
Console.BufferWidth = Math.Min(150, Console.LargestWindowWidth);
|
||||||
|
int width = Console.BufferWidth;
|
||||||
|
int height = Math.Min(50, Console.LargestWindowHeight);
|
||||||
|
Console.SetWindowSize(width, height);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ /* Ignore if not supported */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
namespace ProtosXdemo
|
namespace ProtosXdemo
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
using Modbus.Device;
|
using Modbus.Device;
|
||||||
using static System.Console;
|
|
||||||
|
|
||||||
|
// Writes digital output state to hardware via Modbus.
|
||||||
internal class WriteMachineDigitalStateToIO
|
internal class WriteMachineDigitalStateToIO
|
||||||
{
|
{
|
||||||
public static byte WriteMachineStateToIO(ModbusIpMaster master, ushort coilOutputStartAddress)
|
public static byte WriteMachineStateToIO(ModbusIpMaster master, ushort coilOutputStartAddress)
|
||||||
@@ -17,8 +18,12 @@ namespace ProtosXdemo
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
WriteLine($"Error writing discrete outputs: {ex.Message}");
|
Console.WriteLine($"Error writing discrete outputs: {ex.Message}");
|
||||||
_ = ReadKey(true);
|
if (!Console.IsInputRedirected)
|
||||||
|
{
|
||||||
|
Console.ReadKey(true);
|
||||||
|
}
|
||||||
|
|
||||||
return Data.MachineState.FailureCode[1];
|
return Data.MachineState.FailureCode[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user