diff --git a/px-tcp1/ProtosXDemo/ProtosXdemo.csproj b/px-tcp1/ProtosXDemo/ProtosXdemo.csproj new file mode 100644 index 0000000..8792006 --- /dev/null +++ b/px-tcp1/ProtosXDemo/ProtosXdemo.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + enable + true + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/px-tcp1/ProtosXDemo/ProtosXdemo.sln b/px-tcp1/ProtosXDemo/ProtosXdemo.sln new file mode 100644 index 0000000..7ae7989 --- /dev/null +++ b/px-tcp1/ProtosXDemo/ProtosXdemo.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtosXdemo", "ProtosXdemo.csproj", "{A013091C-203C-48BA-8CD2-317296C4FB44}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A013091C-203C-48BA-8CD2-317296C4FB44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A013091C-203C-48BA-8CD2-317296C4FB44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A013091C-203C-48BA-8CD2-317296C4FB44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A013091C-203C-48BA-8CD2-317296C4FB44}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {86B02D30-0D88-436B-BEAE-84202E5FBFF8} + EndGlobalSection +EndGlobal diff --git a/px-tcp1/ProtosXDemo/README.md b/px-tcp1/ProtosXDemo/README.md new file mode 100644 index 0000000..27ea5c1 --- /dev/null +++ b/px-tcp1/ProtosXDemo/README.md @@ -0,0 +1,128 @@ +# ProtosX Demo Application Documentation + +## Abstract + +The ProtosX Demo Application is designed to interface with the PX-TCP1 bus coupler, allowing users to read and control digital and analog inputs and outputs. This application is structured to provide a way to manage machine states and display input readings. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Configuration](#configuration) +- [Running the Application](#running-the-application) +- [Code Structure](#code-structure) +- [Functionality](#functionality) +- [Error Handling](#error-handling) + +## Requirements + +- **.NET SDK**: Ensure you have the .NET 8 SDK installed on your machine. You can download it from the [official .NET website](https://dotnet.microsoft.com/download). +- **Modbus Device**: A Modbus-compatible PX-TCP1 device connected to your network. +- **Network Access**: Ensure that your application can access the device over the network. + +## Installation + +1. **Clone the Repository**: + ```bash + git clone + cd ProtosXDemo + ``` + +2. **Restore Dependencies**: + ```bash + dotnet restore + ``` + +3. **Build the Application**: + ```bash + dotnet build + ``` + +## Configuration + +Before running the application, you may need to configure the following parameters in the `Program.cs` file: + +- **IP Address**: Set the `IpAddress` variable to the IP address of your Modbus device. +- **Port**: The default Modbus TCP port is `502`. Change it if your device uses a different port. +- **Recipe Name**: Optionally, specify a recipe name for data processing. + +## Running the Application + +To run the application, use the following command in your terminal: + +```bash +dotnet run [recipeName] [timesToRepeat] +``` + +- `recipeName`: (Optional) The name of the recipe to run. +- `timesToRepeat`: (Optional) The number of times to repeat the execution. If not specified, the application will run once. + +### Example Command + +```bash +dotnet run MyRecipe 5 +``` + +## Code Structure + +The application is organized into several classes, each responsible for specific functionalities: + +- **ChannelCache**: Manages the counts of digital and analog inputs and outputs. +- **CommandLineParser**: Parses command-line arguments for the application. +- **ConsoleSetup**: Configures the console appearance. +- **Data**: Contains the machine state and input/output definitions. +- **InputReader**: Reads digital inputs from the Modbus device. +- **FailureCodes**: Initializes failure codes for error handling. +- **LoadMachineStateDigital**: Randomly sets the digital output states. +- **MainEventLoop**: Manages the main execution loop of the application. +- **MakeHardStop**: Safely stops all outputs. +- **OutputExerciser**: Provides methods to turn outputs on and off. +- **RegisterReader**: Reads analog input values from the Modbus device. +- **WriteMachineDigitalStateToIO**: Writes the machine state to the Modbus device. + +## Functionality + +The application performs the following key functions: + +- **Reading Inputs**: It reads digital and analog inputs from the Modbus device and displays their states. +- **Controlling Outputs**: It allows users to turn digital outputs on and off. +- **Error Handling**: The application includes mechanisms to handle various errors, such as connection issues and invalid input. + +## Error Handling + +The application uses predefined failure codes to manage errors effectively. Common error scenarios include: + +- **Connection Errors**: If the application cannot connect to the Modbus device, it will display an error message and exit. +- **Input/Output Errors**: If reading or writing to the device fails, appropriate error messages will be shown. + +### Exit Codes + +- 0 – Success +- 1 – Write-error to hardware +- 2 – Too many command-line arguments +- 255 – Other fatal error + +## Warning + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +`TL;DR: Not my problem` + +## License + +The `GPL V2` license applies to this project. All copyrights belong to their respective copyright holders and all trademarks belong to their trademark holders. + +``` +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.) \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/ChannelCache.cs b/px-tcp1/ProtosXDemo/src/ChannelCache.cs new file mode 100644 index 0000000..427e716 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/ChannelCache.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + internal static class ChannelCache + { + public static ushort DigitalInputCount { get; private set; } + + public static ushort DigitalOutputCount { get; private set; } + + public static ushort AnalogInputCount { get; private set; } + + public static void Initialize() + { + DigitalInputCount = (ushort)Data.MachineState.DigitalInputChannels.Length; + DigitalOutputCount = (ushort)Data.MachineState.DigitalOutputChannels.Length; + 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 new file mode 100644 index 0000000..b623acc --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/CommandLineParser.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using static System.Console; + + internal class CommandLineParser + { + public string? RecipeName { get; } + + public int TimesToRepeat { get; } + + private CommandLineParser(string? recipeName, int timesToRepeat) + { + this.RecipeName = recipeName; + this.TimesToRepeat = timesToRepeat; + } + + public static CommandLineParser Parse(string[] args) + { + return args.Length switch + { + 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(); + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs b/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs new file mode 100644 index 0000000..137c415 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/ConsoleSetup.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using static System.Console; + + internal static class ConsoleSetup + { + public static void ApplyDefaultAppearance() + { + WindowUtility.SetAppearanceOptions(); + WindowUtility.MoveWindowToCenter(); + BackgroundColor = ConsoleColor.DarkBlue; + ForegroundColor = ConsoleColor.White; + Clear(); + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/Data.cs b/px-tcp1/ProtosXDemo/src/Data.cs new file mode 100644 index 0000000..c6c77ce --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/Data.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + public static class Data + { + public class MachineState + { + public static bool[] DigitalInputChannels = new bool[16]; // Digital inputs on PX-149 terminal + public static bool[] DigitalOutputChannels = new bool[16]; // Digital outputs on PX-249 terminal + public static int[] Analogue420Inputs = new int[8]; // Analogue inputs (status and data) 4-20mA on PX-304 + public static int[] Analogue420Outputs = new int[4]; // Analogue outputs (control and data) 4-20mA on PX-404 + public static int[] AnalogueTC1Inputs = new int[4]; // Analogue inputs (status and data) JT/C on PX-332J (first) + public static int[] AnalogueTC2Inputs = new int[4]; // Analogue inputs (status and data) JT/C on PX-332J (second) + public static int[] AnalogueRTD1Inputs = new int[4]; // Analogue inputs (status and data) RTD on PX-322-1 (first) + public static int[] AnalogueRTD2Inputs = new int[4]; // Analogue inputs (status and data) RTD on PX-322-1 (second) + public static byte[] FailureCode = new byte[256]; // 0=success; 1-255=specific failure + public static ushort NumberOfDigitalInputs; // Number of discrete inputs to read + public static ushort NumberOfDigitalOutputs; // Number of discrete outputs to write + public static ushort NumberOfAnalogueInputs; // Number of analogue inputs to read + public static ushort NumberOfAnalogueOutputs; // Number of analogue outputs to write + } + + private enum DigitalInputChannels // Temporary name values + { + DigitalInput0, + DigitalInput1, + DigitalInput2, + DigitalInput3, + DigitalInput4, + DigitalInput5, + DigitalInput6, + DigitalInput7, + DigitalInput8, + DigitalInput9, + DigitalInput10, + DigitalInput11, + DigitalInput12, + DigitalInput13, + DigitalInput14, + DigitalInput15, + } + + private enum DigitalOutputChannels // Application-specific names + { + KitchenLight1, + KitchenLight2, + KitchenLight3, + PorchLight1, + PorchLight2, + OutsideFloodlight1, + OutsideFloodlight2, + PantryLight1, + FrontRoomLight1, + FrontRoomLight2, + FrontRoomLight3, + Radio, + LandingLight1, + ServerRoomLights, + HallLight1, + HallLight2, + } + + private enum AnalogueInputs420 // Temporary name values + { + Input420_1, + Input420_2, + Input420_3, + Input420_4, + } + + private enum AnalogueOutput420 // Temporary name values + { + Output420_1, + Output420_2, + Output420_3, + Output420_4, + } + + private enum AnalogueInputsTC1 // Application-specific names + { + TestTcTemp1_1, + TestTcTemp1_2, + TestTcTemp1_3, + TestTcTemp1_4, + } + + private enum AnalogueInputsTC2 // Application-specific names + { + TestTcTemp2_1, + TestTcTemp2_2, + TestTcTemp2_3, + TestTcTemp2_4, + } + + private enum AnalogueInputsRTD1 // Application-specific names + { + TestRtdTemp1_1, + TestRtdTemp1_2, + TestRtdTemp1_3, + TestRtdTemp1_4, + } + + private enum AnalogueInputsRTD2 // Application-specific names + { + TestRtdTemp2_1, + TestRtdTemp2_2, + TestRtdTemp2_3, + TestRtdTemp2_4, + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/DiscreteReader.cs b/px-tcp1/ProtosXDemo/src/DiscreteReader.cs new file mode 100644 index 0000000..5a95227 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/DiscreteReader.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + internal class DiscreteReader + { + private readonly ModbusIpMaster master; + + public DiscreteReader(ModbusIpMaster master) + { + this.master = master; + } + + public void ReadAll() + { + WriteLine("Reading all discrete inputs…"); + try + { + bool[] inputs = this.master.ReadInputs(0, ChannelCache.DigitalInputCount); + Display(inputs); + } + catch (Exception ex) + { + WriteLine($"[Error] Reading inputs failed → {ex.Message}"); + PromptKey("Press any key to continue…"); + } + } + + private static void Display(bool[] inputs) + { + WriteLine("Discrete Inputs:"); + for (int i = 0; i < inputs.Length; i++) + { + string state = inputs[i] ? "On" : "Off"; + WriteLine($"Input #{i + 1}: {state}"); + } + } + + private static void PromptKey(string prompt) + { + WriteLine(prompt); + 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 new file mode 100644 index 0000000..d9a1587 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/FailureCodes.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + 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[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 new file mode 100644 index 0000000..dd0613f --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/LoadMachineStateDigital.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + internal class LoadMachineStateDigital + { + public static void MakeRandomStateDigital() + { + Data.MachineState.DigitalOutputChannels.Initialize(); + var rand = new Random(); + + for (int i = 0; i < Data.MachineState.NumberOfDigitalOutputs; i++) + { + Data.MachineState.DigitalOutputChannels[i] = rand.Next(1, 99) % 2 == 0; + } + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/MainEventLoop.cs b/px-tcp1/ProtosXDemo/src/MainEventLoop.cs new file mode 100644 index 0000000..8dd700f --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/MainEventLoop.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + 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 + + if (shutdownErrorCode == Data.MachineState.FailureCode[0]) + { + WriteLine("All digital outputs have been turned OFF"); + WriteLine("Program begins running here"); + } + else + { + WriteLine("Program fails here..."); + return shutdownErrorCode; + } + + WriteLine("Main event loop starts here... press the 'Q' key to stop the program run"); + + for (int ndx = 0; ndx < 4; ndx++) + { + if (KeyAvailable) + { + char c = ReadKey(true).KeyChar; + if (c == 'q' || c == 'Q') + { + MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress); + return Data.MachineState.FailureCode[3]; + } + } + + WriteLine("Turning all the outputs ON..."); + for (byte indx = 0; indx < 16; indx++) // Set machine state values to all outputs ON + { + Data.MachineState.DigitalOutputChannels[indx] = true; + } + + if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0]) // Write the machine state values to the I/O hardware + { + WriteLine($"Digital outputs written to I/O at {DateTime.Now:H:mm:ss.fff}"); + } + else + { + WriteLine("Writing machine-state variable failed... press the key to end the program"); + _ = ReadKey(true); + return Data.MachineState.FailureCode[1]; + } + + Thread.Sleep(250); + WriteLine("Turning all the outputs OFF..."); + MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress); + Thread.Sleep(250); + } + + for (int ndx = 0; ndx < 40; ndx++) + { + if (KeyAvailable) + { + char c = ReadKey(true).KeyChar; + if (c == 'q' || c == 'Q') + { + MakeHardStop.HardStop(modbusMaster, coilOutputStartAddress); + return Data.MachineState.FailureCode[3]; + } + } + + LoadMachineStateDigital.MakeRandomStateDigital(); + if (WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress) == Data.MachineState.FailureCode[0]) // Write the machine state values to the I/O hardware + { + WriteLine("All digital outputs are now ON"); + } + else + { + WriteLine("Writing machine-state variable failed... press the key to end the program"); + _ = ReadKey(true); + return Data.MachineState.FailureCode[1]; + } + + Thread.Sleep(250); + } + + 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 new file mode 100644 index 0000000..5c94a4d --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/MakeHardStop.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + 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 + { + Data.MachineState.DigitalOutputChannels[indx] = false; + } + + return WriteMachineDigitalStateToIO.WriteMachineStateToIO(modbusMaster, coilOutputStartAddress); // Write the machine state values to the I/O hardware + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/OutputExerciser.cs b/px-tcp1/ProtosXDemo/src/OutputExerciser.cs new file mode 100644 index 0000000..bdbef36 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/OutputExerciser.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + internal class OutputExerciser + { + private readonly ModbusIpMaster master; + + public OutputExerciser(ModbusIpMaster master) + { + this.master = master; + } + + public void RunSequence() + { + this.SetAllOutputs(false, "Turning all outputs OFF…"); + WaitForKey("Press any key to turn outputs ON…"); + this.SetAllOutputs(true, "Turning all outputs ON…"); + WaitForKey("Press any key to turn outputs OFF and finish…"); + this.SetAllOutputs(false, "Turning all outputs OFF…"); + } + + private void SetAllOutputs(bool state, string message) + { + 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")}"); + } + else + { + WriteLine("[Error] Writing machine state failed"); + WaitForKey("Press any key to exit…"); + Environment.Exit(Data.MachineState.FailureCode[1]); + } + } + + private static void UpdateMachineState(bool state) + { + for (int i = 0; i < Data.MachineState.DigitalOutputChannels.Length; i++) + { + Data.MachineState.DigitalOutputChannels[i] = state; + } + } + + private static void WaitForKey(string prompt) + { + WriteLine(prompt); + 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 new file mode 100644 index 0000000..257b354 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/Program.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using System; + using System.Net.Sockets; + 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 + + public static int Main(string[] args) + { + WindowUtility.SetAppearanceOptions(); // Set console size + WindowUtility.MoveWindowToCenter(); // Set console window in center of display + BackgroundColor = ConsoleColor.DarkBlue; // Add a bit of color + ForegroundColor = ConsoleColor.White; + Clear(); // Start with tabular + + 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(); // Seed for much more comprehensive error handler + TcpClient client = CreateTcpClient(IpAddress, Port); // NModbus initialization + var modbusMaster = ModbusIpMaster.CreateIp(client); + Data.MachineState.NumberOfDigitalInputs = (ushort)Data.MachineState.DigitalInputChannels.Length; + Data.MachineState.NumberOfDigitalOutputs = (ushort)Data.MachineState.DigitalOutputChannels.Length; + + WriteLine("Created Modbus master."); + ReadDiscreteInputs(modbusMaster); + WriteLine("All digital inputs have been read"); + WriteLine("\nMain event loop begins...\n"); + + if (MainEventLoop.RunEventLoop(modbusMaster, CoilOutputStartAddress) != Data.MachineState.FailureCode[0]) + { + return Data.MachineState.FailureCode[3]; + } + + int shutdownErrorCode = MakeHardStop.HardStop(modbusMaster, CoilOutputStartAddress); // Write the machine state values to the I/O hardware and check for success + return shutdownErrorCode; + } + + private static TcpClient CreateTcpClient(string ipAddress, int port) + { + try + { + return new TcpClient(ipAddress, port); + } + 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")}"); + } + } + } +} \ No newline at end of file diff --git a/px-tcp1/ProtosXDemo/src/RegisterReader.cs b/px-tcp1/ProtosXDemo/src/RegisterReader.cs new file mode 100644 index 0000000..87731b8 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/RegisterReader.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + 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 + + 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 + for (int iteration = 0; iteration < 42; iteration++) + { + // Read thermocouple values + ushort[] tCValues = master.ReadInputRegisters(TCRegisterStartAddress, TCNumberOfPoints); + + // 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); + + // 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…"); + try + { + ushort[] registers = master.ReadInputRegisters(0, ChannelCache.AnalogInputCount); + Display(registers); + bool[] bits = ConvertRegistersToBools(registers); + Display(bits); + } + catch (Exception ex) + { + WriteLine($"[Error] Reading input registers failed → {ex.Message}"); + PromptKey("Press any key to continue…"); + } + } + + private static void Display(ushort[] registers) + { + WriteLine("Input Registers (ushort):"); + for (int i = 0; i < registers.Length; i++) + { + WriteLine($"Register #{i + 1}: {registers[i]}"); + } + } + + private static void Display(bool[] bits) + { + WriteLine("Flattened Bits:"); + for (int i = 0; i < bits.Length; i++) + { + 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(); + } + + private static void PromptKey(string prompt) + { + WriteLine(prompt); + 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 new file mode 100644 index 0000000..1ff7820 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/WindowUtility.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using System.Runtime.InteropServices; + using static System.Console; + + internal static class WindowUtility + { + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [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 static Size GetScreenSize() => new Size(GetSystemMetrics(0), GetSystemMetrics(1)); + + private struct Size + { + public int Width { get; set; } + + public int Height { get; set; } + + public Size(int width, int height) + { + this.Width = width; + this.Height = height; + } + } + + [DllImport("kernel32.dll", ExactSpelling = true)] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] + private static extern int GetSystemMetrics(int nIndex); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowRect(HandleRef hWnd, out Rect lpRect); + + [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 + } + + private static Size GetWindowSize(IntPtr window) + { + if (!GetWindowRect(new HandleRef(null, window), out Rect rect)) + { + 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); + } + + public static void MoveWindowToCenter() + { + IntPtr window = GetConsoleWindow(); + if (window == IntPtr.Zero) + { + throw new Exception("Couldn't find a window to center!"); + } + + 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 + { + BufferWidth = 150; + SetWindowSize(BufferWidth, 50); + } + } + } +} diff --git a/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs b/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs new file mode 100644 index 0000000..79940b5 --- /dev/null +++ b/px-tcp1/ProtosXDemo/src/WriteMachineDigitalStateToIO.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace ProtosXdemo +{ + using Modbus.Device; + using static System.Console; + + internal class WriteMachineDigitalStateToIO + { + public static byte WriteMachineStateToIO(ModbusIpMaster master, ushort coilOutputStartAddress) + { + try + { + master.WriteMultipleCoils(coilOutputStartAddress, Data.MachineState.DigitalOutputChannels); + } + catch (Exception ex) + { + WriteLine($"Error writing discrete outputs: {ex.Message}"); + _ = ReadKey(true); + return Data.MachineState.FailureCode[1]; + } + + return Data.MachineState.FailureCode[0]; + } + } +}