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:
2025-11-11 22:12:27 +00:00
parent 71f4a0be92
commit 0f4609e5d3
16 changed files with 580 additions and 214 deletions

View File

@@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<Nullable>enable</Nullable>
</PropertyGroup>
</PropertyGroup>
<ItemGroup>
<None Remove="ProtosXdemo.zip" />
@@ -14,16 +14,19 @@
</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="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>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,7 @@
namespace ProtosXdemo
{
// Stores counts of I/O channels for easy access.
internal static class ChannelCache
{
public static ushort DigitalInputCount { get; private set; }
@@ -12,6 +13,7 @@ namespace ProtosXdemo
public static ushort AnalogInputCount { get; private set; }
// Initializes counts from Data.MachineState arrays.
public static void Initialize()
{
DigitalInputCount = (ushort)Data.MachineState.DigitalInputChannels.Length;
@@ -19,4 +21,4 @@ namespace ProtosXdemo
AnalogInputCount = (ushort)Data.MachineState.NumberOfAnalogueInputs;
}
}
}
}

View File

@@ -4,15 +4,16 @@
namespace ProtosXdemo
{
using static System.Console;
using System;
// Parses command-line arguments for recipe name and repeat count.
internal class CommandLineParser
{
public string? RecipeName { get; }
public string RecipeName { get; }
public int TimesToRepeat { get; }
private CommandLineParser(string? recipeName, int timesToRepeat)
private CommandLineParser(string recipeName, int timesToRepeat)
{
this.RecipeName = recipeName;
this.TimesToRepeat = timesToRepeat;
@@ -20,20 +21,20 @@ namespace ProtosXdemo
public static CommandLineParser Parse(string[] args)
{
return args.Length switch
switch (args.Length)
{
0 => new CommandLineParser(null, 0),
1 => new CommandLineParser(args[0], 0),
2 => new CommandLineParser(args[0], Convert.ToInt32(args[1])),
_ => ExitOnTooManyArgs()
};
}
private static CommandLineParser ExitOnTooManyArgs()
{
WriteLine("Too many command-line arguments; exiting.");
Environment.Exit(Data.MachineState.FailureCode[2]);
throw new OperationCanceledException();
case 0:
return new CommandLineParser(null, 0);
case 1:
return new CommandLineParser(args[0], 0);
case 2:
int repeatCount = Convert.ToInt32(args[1]);
return new CommandLineParser(args[0], repeatCount);
default:
Console.WriteLine("Too many command-line arguments; exiting.");
Environment.Exit(Data.MachineState.FailureCode[2]);
return null;
}
}
}
}
}

View File

@@ -4,17 +4,21 @@
namespace ProtosXdemo
{
using static System.Console;
using System;
// Sets up console appearance (size, color) — Windows only.
internal static class ConsoleSetup
{
public static void ApplyDefaultAppearance()
{
WindowUtility.SetAppearanceOptions();
WindowUtility.MoveWindowToCenter();
BackgroundColor = ConsoleColor.DarkBlue;
ForegroundColor = ConsoleColor.White;
Clear();
if (OperatingSystem.IsWindows())
{
WindowUtility.SetAppearanceOptions();
WindowUtility.MoveWindowToCenter();
Console.BackgroundColor = ConsoleColor.DarkBlue;
Console.ForegroundColor = ConsoleColor.White;
Console.Clear();
}
}
}
}
}

View 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);
}
});
}
}
}

View 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);
}
}
}

View File

@@ -4,9 +4,10 @@
namespace ProtosXdemo
{
using System;
using Modbus.Device;
using static System.Console;
// Reads and displays digital input states via Modbus.
internal class DiscreteReader
{
private readonly ModbusIpMaster master;
@@ -18,7 +19,7 @@ namespace ProtosXdemo
public void ReadAll()
{
WriteLine("Reading all discrete inputs…");
Console.WriteLine("Reading all discrete inputs…");
try
{
bool[] inputs = this.master.ReadInputs(0, ChannelCache.DigitalInputCount);
@@ -26,25 +27,28 @@ namespace ProtosXdemo
}
catch (Exception ex)
{
WriteLine($"[Error] Reading inputs failed → {ex.Message}");
Console.WriteLine($"[Error] Reading inputs failed → {ex.Message}");
PromptKey("Press any key to continue…");
}
}
private static void Display(bool[] inputs)
{
WriteLine("Discrete Inputs:");
Console.WriteLine("Discrete Inputs:");
for (int i = 0; i < inputs.Length; i++)
{
string state = inputs[i] ? "On" : "Off";
WriteLine($"Input #{i + 1}: {state}");
Console.WriteLine($"Input #{i + 1}: {state}");
}
}
private static void PromptKey(string prompt)
{
WriteLine(prompt);
ReadKey(intercept: true);
if (Console.IsInputRedirected == false)
{
Console.WriteLine(prompt);
Console.ReadKey(intercept: true);
}
}
}
}
}

View File

@@ -4,15 +4,16 @@
namespace ProtosXdemo
{
// Initializes standard failure codes.
internal class FailureCodes
{
public static void FailureCodeValues()
{
Data.MachineState.FailureCode[0] = 0; // success
Data.MachineState.FailureCode[1] = 1; // digital write error from hardware
Data.MachineState.FailureCode[2] = 2; // too many CLI parameters
Data.MachineState.FailureCode[3] = 3; // program was interrupted
Data.MachineState.FailureCode[0] = 0; // success
Data.MachineState.FailureCode[1] = 1; // digital write error
Data.MachineState.FailureCode[2] = 2; // too many CLI args
Data.MachineState.FailureCode[3] = 3; // interrupted
Data.MachineState.FailureCode[255] = 255; // other error
}
}
}
}

View File

@@ -4,17 +4,19 @@
namespace ProtosXdemo
{
using System;
// Loads random state into digital outputs.
internal class LoadMachineStateDigital
{
public static void MakeRandomStateDigital()
{
Data.MachineState.DigitalOutputChannels.Initialize();
var rand = new Random();
Array.Clear(Data.MachineState.DigitalOutputChannels, 0, Data.MachineState.DigitalOutputChannels.Length);
Random rand = new Random();
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;
}
}
}
}
}

View File

@@ -4,34 +4,36 @@
namespace ProtosXdemo
{
using System;
using System.Threading;
using Modbus.Device;
using static System.Console;
// Main control loop for digital output sequences.
internal class MainEventLoop
{
public static int RunEventLoop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
{
WriteLine("\nTurning all the outputs OFF..."); // Start with known state = all outputs OFF
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress); // Write the machine state values to the I/O hardware and check for success
Console.WriteLine("\nTurning all outputs OFF...");
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
if (shutdownErrorCode == Data.MachineState.FailureCode[0])
{
WriteLine("All digital outputs have been turned OFF");
WriteLine("Program begins running here");
Console.WriteLine("All digital outputs OFF");
Console.WriteLine("Program begins running here");
}
else
{
WriteLine("Program fails here...");
Console.WriteLine("Program fails here...");
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++)
{
if (KeyAvailable)
if (Console.KeyAvailable)
{
char c = ReadKey(true).KeyChar;
char c = Console.ReadKey(true).KeyChar;
if (c == 'q' || c == 'Q')
{
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
@@ -39,34 +41,40 @@ namespace ProtosXdemo
}
}
WriteLine("Turning all the outputs ON...");
for (byte indx = 0; indx < 16; indx++) // Set machine state values to all outputs ON
Console.WriteLine("Turning all outputs ON...");
for (byte indx = 0; indx < 16; indx++)
{
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
{
WriteLine("Writing machine-state variable failed... press the <ANY> key to end the program");
_ = ReadKey(true);
Console.WriteLine("Writing machine state failed... press any key to end");
if (!Console.IsInputRedirected)
{
Console.ReadKey(true);
}
return Data.MachineState.FailureCode[1];
}
Thread.Sleep(250);
WriteLine("Turning all the outputs OFF...");
Console.WriteLine("Turning all outputs OFF...");
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
Thread.Sleep(250);
}
// Sequence 2: Random states
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')
{
MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress);
@@ -75,14 +83,18 @@ namespace ProtosXdemo
}
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
{
WriteLine("Writing machine-state variable failed... press the <ANY> key to end the program");
_ = ReadKey(true);
Console.WriteLine("Writing machine state failed... press any key to end");
if (!Console.IsInputRedirected)
{
Console.ReadKey(true);
}
return Data.MachineState.FailureCode[1];
}
@@ -92,4 +104,4 @@ namespace ProtosXdemo
return Data.MachineState.FailureCode[0];
}
}
}
}

View File

@@ -5,19 +5,19 @@
namespace ProtosXdemo
{
using Modbus.Device;
using static System.Console;
// Turns all digital outputs OFF.
internal class MakeHardStop
{
public static int HardStop(ModbusIpMaster modbusMaster, ushort coilOutputStartAddress)
{
WriteLine("Turning all the outputs OFF...");
for (byte indx = 0; indx < 16; indx++) // Set machine state values to all outputs OFF
Console.WriteLine("Turning all outputs OFF...");
for (byte indx = 0; indx < 16; indx++)
{
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);
}
}
}
}

View File

@@ -4,9 +4,10 @@
namespace ProtosXdemo
{
using System;
using Modbus.Device;
using static System.Console;
// Simple ON/OFF sequence for testing.
internal class OutputExerciser
{
private readonly ModbusIpMaster master;
@@ -27,17 +28,16 @@ namespace ProtosXdemo
private void SetAllOutputs(bool state, string message)
{
WriteLine(message);
Console.WriteLine(message);
UpdateMachineState(state);
int result = WriteMachineDigitalStateToIO.WriteMachineStateToIO(this.master, 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
{
WriteLine("[Error] Writing machine state failed");
Console.WriteLine("[Error] Writing machine state failed");
WaitForKey("Press any key to exit…");
Environment.Exit(Data.MachineState.FailureCode[1]);
}
@@ -53,8 +53,11 @@ namespace ProtosXdemo
private static void WaitForKey(string prompt)
{
WriteLine(prompt);
ReadKey(intercept: true);
if (Console.IsInputRedirected == false)
{
Console.WriteLine(prompt);
Console.ReadKey(intercept: true);
}
}
}
}
}

View File

@@ -5,64 +5,56 @@
namespace ProtosXdemo
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using Modbus.Device;
using static System.Console;
public class Program
{
private static readonly string IpAddress = "10.10.1.1"; // Replace with the device's IP address
private static readonly int Port = 502; // Default Modbus TCP port
private static readonly ushort CoilInputStartAddress = 0; // Starting address for digital inputs
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
private static readonly string IpAddress = "10.10.1.1";
private static readonly int Port = 502;
private static readonly ushort CoilOutputStartAddress = 0;
public static int Main(string[] args)
public static async Task<int> Main(string[] args)
{
WindowUtility.SetAppearanceOptions(); // Set console size
WindowUtility.MoveWindowToCenter(); // Set console window in center of display
BackgroundColor = ConsoleColor.DarkBlue; // Add a bit of color
ForegroundColor = ConsoleColor.White;
Clear(); // Start with tabular
ConsoleSetup.ApplyDefaultAppearance();
var cli = CommandLineParser.Parse(args);
string recipeName = cli.RecipeName;
int timesToRepeat = cli.TimesToRepeat;
switch (args.Length)
{
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();
ChannelCache.Initialize();
FailureCodes.FailureCodeValues(); // Seed for much more comprehensive error handler
TcpClient client = CreateTcpClient(IpAddress, Port); // NModbus initialization
TcpClient client = CreateTcpClient(IpAddress, Port);
var modbusMaster = ModbusIpMaster.CreateIp(client);
Data.MachineState.NumberOfDigitalInputs = (ushort)Data.MachineState.DigitalInputChannels.Length;
Data.MachineState.NumberOfDigitalOutputs = (ushort)Data.MachineState.DigitalOutputChannels.Length;
Console.WriteLine("Created Modbus master.");
WriteLine("Created Modbus master.");
ReadDiscreteInputs(modbusMaster);
WriteLine("All digital inputs have been read");
WriteLine("\nMain event loop begins...\n");
string logPath = "machine_log.csv";
var logger = new CsvDataLogger(logPath);
logger.Start();
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
return shutdownErrorCode;
int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, CoilOutputStartAddress);
client?.Close();
return result == 0 ? shutdownErrorCode : result;
}
private static TcpClient CreateTcpClient(string ipAddress, int port)
@@ -73,33 +65,10 @@ namespace ProtosXdemo
}
catch (Exception ex)
{
WriteLine($"Error creating TCP client: {ex.Message}");
throw; // Rethrow to handle it in the Main method
}
}
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")}");
Console.WriteLine($"Error creating TCP client: {ex.Message}");
Environment.Exit(Data.MachineState.FailureCode[255]);
return null;
}
}
}
}
}

View File

@@ -4,89 +4,91 @@
namespace ProtosXdemo
{
using System;
using System.Threading;
using Modbus.Device;
using static System.Console;
// Reads analog input registers (TC, RTD, etc.).
public class RegisterReader
{
private static readonly ModbusIpMaster Master;
private static readonly ushort TCRegisterStartAddress = 17; // Starting address for reading PX-332-J inputs
private static readonly ushort RTDRegisterStartAddress = 25; // Starting address for reading PX-322-1 inputs
private static readonly ushort TCNumberOfPoints = 8; // Number of values to read
private static readonly ushort RTDNumberOfPoints = 6; // Number of values to read
private static readonly ushort TCRegisterStartAddress = 17;
private static readonly ushort RTDRegisterStartAddress = 25;
private static readonly ushort TCNumberOfPoints = 8;
private static readonly ushort RTDNumberOfPoints = 6;
public static void ReadAll(ModbusIpMaster master)
{
// Display header for temperature readings
WriteLine("Reading and displaying all analogue input values in engineering units");
// Read and display temperature values for 42 iterations
Console.WriteLine("Reading analogue input values in engineering units");
for (int iteration = 0; iteration < 42; iteration++)
{
// Read thermocouple values
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);
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);
}
WriteLine("Reading all input registers…");
Console.WriteLine("Reading all input registers…");
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);
bool[] bits = ConvertRegistersToBools(registers);
Display(bits);
}
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…");
}
}
private static void Display(ushort[] registers)
{
WriteLine("Input Registers (ushort):");
Console.WriteLine("Input Registers (ushort):");
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)
{
WriteLine("Flattened Bits:");
Console.WriteLine("Flattened Bits:");
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)
{
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)
{
WriteLine(prompt);
ReadKey(intercept: true);
if (Console.IsInputRedirected == false)
{
Console.WriteLine(prompt);
Console.ReadKey(intercept: true);
}
}
}
}
}

View File

@@ -1,12 +1,13 @@
// <copyright file="WindowUtility.cs" company="PlaceholderCompany">
// <copyright file="WindowUtility.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace ProtosXdemo
{
using System;
using System.Runtime.InteropServices;
using static System.Console;
// Windows-only console window manipulation.
internal static class WindowUtility
{
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
@@ -15,8 +16,8 @@ namespace ProtosXdemo
[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 const uint SWPNOSIZE = 0x0001;
private const uint SWPNOZORDER = 0x0004;
private const uint SWPNOSIZE = 0x0001;
private static Size GetScreenSize() => new Size(GetSystemMetrics(0), GetSystemMetrics(1));
@@ -46,10 +47,10 @@ namespace ProtosXdemo
[StructLayout(LayoutKind.Sequential)]
private struct Rect
{
public int Left; // x position of upper-left corner
public int Top; // y position of upper-left corner
public int Right; // x position of lower-right corner
public int Bottom; // y position of lower-right corner
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private static Size GetWindowSize(IntPtr window)
@@ -59,35 +60,43 @@ namespace ProtosXdemo
throw new Exception("Unable to get window rect!");
}
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
return new Size(width, height);
return new Size(rect.Right - rect.Left, rect.Bottom - rect.Top);
}
public static void MoveWindowToCenter()
{
if (!OperatingSystem.IsWindows())
{
return;
}
IntPtr window = GetConsoleWindow();
if (window == IntPtr.Zero)
{
throw new Exception("Couldn't find a window to center!");
return;
}
Size screenSize = GetScreenSize();
Size windowSize = GetWindowSize(window);
int x = (screenSize.Width - windowSize.Width) / 2;
int y = (screenSize.Height - windowSize.Height) / 2;
SetWindowPos(window, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER);
}
public static void SetAppearanceOptions()
{
if (!IsOutputRedirected) // set console window size
if (!Console.IsOutputRedirected && OperatingSystem.IsWindows())
{
BufferWidth = 150;
SetWindowSize(BufferWidth, 50);
try
{
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 */
}
}
}
}

View File

@@ -4,9 +4,10 @@
namespace ProtosXdemo
{
using System;
using Modbus.Device;
using static System.Console;
// Writes digital output state to hardware via Modbus.
internal class WriteMachineDigitalStateToIO
{
public static byte WriteMachineStateToIO(ModbusIpMaster master, ushort coilOutputStartAddress)
@@ -17,12 +18,16 @@ namespace ProtosXdemo
}
catch (Exception ex)
{
WriteLine($"Error writing discrete outputs: {ex.Message}");
_ = ReadKey(true);
Console.WriteLine($"Error writing discrete outputs: {ex.Message}");
if (!Console.IsInputRedirected)
{
Console.ReadKey(true);
}
return Data.MachineState.FailureCode[1];
}
return Data.MachineState.FailureCode[0];
}
}
}
}