diff --git a/px-tcp1/ProtosXDemo/ProtosXdemo.csproj b/px-tcp1/ProtosXDemo/ProtosXdemo.csproj
index 8792006..6c181d9 100644
--- a/px-tcp1/ProtosXDemo/ProtosXdemo.csproj
+++ b/px-tcp1/ProtosXDemo/ProtosXdemo.csproj
@@ -6,7 +6,7 @@
enable
true
enable
-
+
@@ -14,16 +14,19 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
diff --git a/px-tcp1/ProtosXDemo/src/ChannelCache.cs b/px-tcp1/ProtosXDemo/src/ChannelCache.cs
index 427e716..8f8c6df 100644
--- a/px-tcp1/ProtosXDemo/src/ChannelCache.cs
+++ b/px-tcp1/ProtosXDemo/src/ChannelCache.cs
@@ -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;
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/CommandLineParser.cs b/px-tcp1/ProtosXDemo/src/CommandLineParser.cs
index b623acc..d750050 100644
--- a/px-tcp1/ProtosXDemo/src/CommandLineParser.cs
+++ b/px-tcp1/ProtosXDemo/src/CommandLineParser.cs
@@ -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;
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs b/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs
index 137c415..0961536 100644
--- a/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs
+++ b/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs
@@ -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();
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/CsvDataLogger.cs b/px-tcp1/ProtosXDemo/src/CsvDataLogger.cs
new file mode 100644
index 0000000..e05d772
--- /dev/null
+++ b/px-tcp1/ProtosXDemo/src/CsvDataLogger.cs
@@ -0,0 +1,230 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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);
+ }
+ });
+ }
+ }
+}
diff --git a/px-tcp1/ProtosXDemo/src/CsvPlotter.cs b/px-tcp1/ProtosXDemo/src/CsvPlotter.cs
new file mode 100644
index 0000000..8ff27d3
--- /dev/null
+++ b/px-tcp1/ProtosXDemo/src/CsvPlotter.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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 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());
+ var timestamps = new List();
+
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/px-tcp1/ProtosXDemo/src/DiscreteReader.cs b/px-tcp1/ProtosXDemo/src/DiscreteReader.cs
index 5a95227..6a19910 100644
--- a/px-tcp1/ProtosXDemo/src/DiscreteReader.cs
+++ b/px-tcp1/ProtosXDemo/src/DiscreteReader.cs
@@ -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);
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/FailureCodes.cs b/px-tcp1/ProtosXDemo/src/FailureCodes.cs
index d9a1587..9bcb26d 100644
--- a/px-tcp1/ProtosXDemo/src/FailureCodes.cs
+++ b/px-tcp1/ProtosXDemo/src/FailureCodes.cs
@@ -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
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/LoadMachineStateDigital.cs b/px-tcp1/ProtosXDemo/src/LoadMachineStateDigital.cs
index dd0613f..5d21041 100644
--- a/px-tcp1/ProtosXDemo/src/LoadMachineStateDigital.cs
+++ b/px-tcp1/ProtosXDemo/src/LoadMachineStateDigital.cs
@@ -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;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/MainEventLoop.cs b/px-tcp1/ProtosXDemo/src/MainEventLoop.cs
index 8dd700f..025dfb6 100644
--- a/px-tcp1/ProtosXDemo/src/MainEventLoop.cs
+++ b/px-tcp1/ProtosXDemo/src/MainEventLoop.cs
@@ -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 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 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];
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/MakeHardStop.cs b/px-tcp1/ProtosXDemo/src/MakeHardStop.cs
index 5c94a4d..52298a0 100644
--- a/px-tcp1/ProtosXDemo/src/MakeHardStop.cs
+++ b/px-tcp1/ProtosXDemo/src/MakeHardStop.cs
@@ -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);
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/OutputExerciser.cs b/px-tcp1/ProtosXDemo/src/OutputExerciser.cs
index bdbef36..a0ba380 100644
--- a/px-tcp1/ProtosXDemo/src/OutputExerciser.cs
+++ b/px-tcp1/ProtosXDemo/src/OutputExerciser.cs
@@ -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);
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/Program.cs b/px-tcp1/ProtosXDemo/src/Program.cs
index 257b354..0c30df3 100644
--- a/px-tcp1/ProtosXDemo/src/Program.cs
+++ b/px-tcp1/ProtosXDemo/src/Program.cs
@@ -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 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;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/RegisterReader.cs b/px-tcp1/ProtosXDemo/src/RegisterReader.cs
index 87731b8..80c05b0 100644
--- a/px-tcp1/ProtosXDemo/src/RegisterReader.cs
+++ b/px-tcp1/ProtosXDemo/src/RegisterReader.cs
@@ -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();
+ 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);
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/px-tcp1/ProtosXDemo/src/WindowUtility.cs b/px-tcp1/ProtosXDemo/src/WindowUtility.cs
index 1ff7820..5baf1c6 100644
--- a/px-tcp1/ProtosXDemo/src/WindowUtility.cs
+++ b/px-tcp1/ProtosXDemo/src/WindowUtility.cs
@@ -1,12 +1,13 @@
-//
+//
// Copyright (c) PlaceholderCompany. All rights reserved.
//
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 */
+ }
}
}
}
diff --git a/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs b/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs
index 79940b5..f60cf07 100644
--- a/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs
+++ b/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs
@@ -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];
}
}
-}
+}
\ No newline at end of file