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

@@ -14,6 +14,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NModbus4" Version="2.1.0" />
<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"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -23,7 +27,6 @@
<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>

View File

@@ -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;

View File

@@ -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]);
throw new OperationCanceledException(); return null;
}
} }
} }
} }

View File

@@ -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()
{
if (OperatingSystem.IsWindows())
{ {
WindowUtility.SetAppearanceOptions(); WindowUtility.SetAppearanceOptions();
WindowUtility.MoveWindowToCenter(); WindowUtility.MoveWindowToCenter();
BackgroundColor = ConsoleColor.DarkBlue; Console.BackgroundColor = ConsoleColor.DarkBlue;
ForegroundColor = ConsoleColor.White; Console.ForegroundColor = ConsoleColor.White;
Clear(); 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 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);
}
} }
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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;
} }
} }
} }

View File

@@ -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];
} }

View File

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

View File

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

View File

@@ -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")}");
} }
} }
} }

View File

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

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 (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 */
}
} }
} }
} }

View File

@@ -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];
} }