From 0f4609e5d3f0f7643b160ce34e0c02ab2592de7b Mon Sep 17 00:00:00 2001 From: mharb Date: Tue, 11 Nov 2025 22:12:27 +0000 Subject: [PATCH] Implement Basic Logging to File Attempt at dumping values from memory into a CSV file for post-processing Signed-off-by: mharb --- px-tcp1/ProtosXDemo/ProtosXdemo.csproj | 17 +- px-tcp1/ProtosXDemo/src/ChannelCache.cs | 4 +- px-tcp1/ProtosXDemo/src/CommandLineParser.cs | 35 +-- px-tcp1/ProtosXDemo/src/ConsoleSetup.cs | 18 +- px-tcp1/ProtosXDemo/src/CsvDataLogger.cs | 230 ++++++++++++++++++ px-tcp1/ProtosXDemo/src/CsvPlotter.cs | 119 +++++++++ px-tcp1/ProtosXDemo/src/DiscreteReader.cs | 20 +- px-tcp1/ProtosXDemo/src/FailureCodes.cs | 11 +- .../src/LoadMachineStateDigital.cs | 12 +- px-tcp1/ProtosXDemo/src/MainEventLoop.cs | 60 +++-- px-tcp1/ProtosXDemo/src/MakeHardStop.cs | 10 +- px-tcp1/ProtosXDemo/src/OutputExerciser.cs | 19 +- px-tcp1/ProtosXDemo/src/Program.cs | 109 +++------ px-tcp1/ProtosXDemo/src/RegisterReader.cs | 74 +++--- px-tcp1/ProtosXDemo/src/WindowUtility.cs | 43 ++-- .../src/WriteMachineDigitalStateToIO.cs | 13 +- 16 files changed, 580 insertions(+), 214 deletions(-) create mode 100644 px-tcp1/ProtosXDemo/src/CsvDataLogger.cs create mode 100644 px-tcp1/ProtosXDemo/src/CsvPlotter.cs 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