diff --git a/EasyModbus.sln b/EasyModbus.sln index 2ec15b3..f19c59c 100644 --- a/EasyModbus.sln +++ b/EasyModbus.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29503.13 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyModbus_Net40", "EasyModbus\EasyModbus_Net40.csproj", "{7657FC4A-AEDF-4F17-B3E9-0D0DFB1CE23B}" EndProject @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppStd", "ConsoleApp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyModbus_V5.0", "EasyModbus_NET5\EasyModbus_V5.0.csproj", "{83AFFC60-BC95-46B1-861F-B1F80270FBFA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyModbus_Net60", "EasyModbus_Net60\EasyModbus_Net60.csproj", "{6BD5A885-D0A3-44C0-BF73-E2118519EAFE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -331,12 +333,60 @@ Global {83AFFC60-BC95-46B1-861F-B1F80270FBFA}.Release|x64.Build.0 = Release|Any CPU {83AFFC60-BC95-46B1-861F-B1F80270FBFA}.Release|x86.ActiveCfg = Release|Any CPU {83AFFC60-BC95-46B1-861F-B1F80270FBFA}.Release|x86.Build.0 = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|ARM.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|x64.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Debug|x86.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|Any CPU.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|Any CPU.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|ARM.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|ARM.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|x64.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|x64.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|x86.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial|x86.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|Any CPU.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|Any CPU.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|ARM.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|ARM.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|x64.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|x64.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|x86.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial1|x86.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|Any CPU.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|Any CPU.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|ARM.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|ARM.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|x64.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|x64.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|x86.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial2|x86.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|Any CPU.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|Any CPU.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|ARM.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|ARM.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|x64.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|x64.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|x86.ActiveCfg = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.DebugCommercial3|x86.Build.0 = Debug|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|Any CPU.Build.0 = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|ARM.ActiveCfg = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|ARM.Build.0 = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|x64.ActiveCfg = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|x64.Build.0 = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|x86.ActiveCfg = Release|Any CPU + {6BD5A885-D0A3-44C0-BF73-E2118519EAFE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7628B1C9-8DDB-4DBD-A71D-D369A4A166DE} VisualSVNWorkingCopyRoot = . + SolutionGuid = {7628B1C9-8DDB-4DBD-A71D-D369A4A166DE} EndGlobalSection EndGlobal diff --git a/EasyModbus/ModbusClient.cs b/EasyModbus/ModbusClient.cs index c1e8f38..b9e0285 100644 --- a/EasyModbus/ModbusClient.cs +++ b/EasyModbus/ModbusClient.cs @@ -28,6 +28,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Reflection; using System.Text; using System.Collections.Generic; +using System.Threading; namespace EasyModbus { @@ -941,11 +942,12 @@ public bool[] ReadDiscreteInputs(int startingAddress, int quantity) DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; - + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { - while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + sw_delay.SpinOnce(); + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; @@ -1135,10 +1137,13 @@ public bool[] ReadCoils(int startingAddress, int quantity) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { - while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + sw_delay.SpinOnce(); + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); @@ -1326,11 +1331,14 @@ public int[] ReadHoldingRegisters(int startingAddress, int quantity) DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; @@ -1528,12 +1536,14 @@ public int[] ReadInputRegisters(int startingAddress, int quantity) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; - + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } @@ -1729,11 +1739,14 @@ public void WriteSingleCoil(int startingAddress, bool value) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } @@ -1908,11 +1921,14 @@ public void WriteSingleRegister(int startingAddress, int value) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } @@ -2104,11 +2120,14 @@ public void WriteMultipleCoils(int startingAddress, bool[] values) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } @@ -2289,11 +2308,14 @@ public void WriteMultipleRegisters(int startingAddress, int[] values) readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } @@ -2492,11 +2514,14 @@ public int[] ReadWriteMultipleRegisters(int startingAddressRead, int quantityRea readBuffer = new byte[256]; DateTime dateTimeSend = DateTime.Now; byte receivedUnitIdentifier = 0xFF; + + SpinWait sw_delay = new SpinWait(); while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) { while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) - System.Threading.Thread.Sleep(1); - data = new byte[2100]; + sw_delay.SpinOnce(); + + data = new byte[2100]; Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); receivedUnitIdentifier = data[6]; } diff --git a/EasyModbusAdvancedClient/AddConnectionForm.cs b/EasyModbusAdvancedClient/AddConnectionForm.cs index 6b74d1e..a430b1b 100644 --- a/EasyModbusAdvancedClient/AddConnectionForm.cs +++ b/EasyModbusAdvancedClient/AddConnectionForm.cs @@ -82,16 +82,6 @@ void Button1Click(object sender, EventArgs e) if (!editMode) { easyModbusManager.AddConnection(connectionProperties); - if (connectionProperties.ModbusTypeProperty == ModbusType.ModbusTCP) - { - connectionProperties.modbusClient = new EasyModbus.ModbusClient(); - connectionProperties.modbusClient.UnitIdentifier = (byte)connectionProperties.SlaveID; - } - else - { - connectionProperties.modbusClient = new EasyModbus.ModbusClient(connectionProperties.ComPort); - connectionProperties.modbusClient.UnitIdentifier = (byte)connectionProperties.SlaveID; - } } else easyModbusManager.EditConnection(connectionProperties, indexToEdit); diff --git a/EasyModbusAdvancedClient/EasyModbusManager.cs b/EasyModbusAdvancedClient/EasyModbusManager.cs index f0d8d01..2a7270b 100644 --- a/EasyModbusAdvancedClient/EasyModbusManager.cs +++ b/EasyModbusAdvancedClient/EasyModbusManager.cs @@ -29,6 +29,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Xml.Linq; using System.Linq; using System.Xml; +using EasyModbus; namespace EasyModbusAdvancedClient { @@ -54,6 +55,19 @@ public void AddConnection(ConnectionProperties connectionProperties) throw new Exception("Duplicate connection Name detected"); } } + + // create modbus client accordingly + if (connectionProperties.ModbusTypeProperty == ModbusType.ModbusTCP) + { + connectionProperties.modbusClient = new EasyModbus.ModbusClient(); + connectionProperties.modbusClient.UnitIdentifier = (byte)connectionProperties.SlaveID; + } + else + { + connectionProperties.modbusClient = new EasyModbus.ModbusClient(connectionProperties.ComPort); + connectionProperties.modbusClient.UnitIdentifier = (byte)connectionProperties.SlaveID; + } + connectionPropertiesList.Add(connectionProperties); if (connectionPropertiesListChanged != null) connectionPropertiesListChanged(this); @@ -81,6 +95,9 @@ public void EditConnection(ConnectionProperties connectionProperty, int connecti public void AddFunctionProperty(FunctionProperties functionProperty, int connectionNumber) { + // create link to connection + functionProperty.Connection = connectionPropertiesList[connectionNumber]; + // add to list connectionPropertiesList[connectionNumber].FunctionPropertiesList.Add(functionProperty); if (connectionPropertiesListChanged != null) connectionPropertiesListChanged(this); @@ -113,18 +130,18 @@ public void GetValues(ConnectionProperties connectionProperties, int functionPro modbusClient.Connect(); } - switch (connectionProperties.FunctionPropertiesList[functionPropertyID].FunctionCode) + switch (connectionProperties.FunctionPropertiesList[functionPropertyID].FunctionCodeRead) { - case FunctionCode.ReadCoils: + case FunctionCodeRd.ReadCoils: connectionProperties.FunctionPropertiesList[functionPropertyID].values = modbusClient.ReadCoils(connectionProperties.FunctionPropertiesList[functionPropertyID].StartingAdress, connectionProperties.FunctionPropertiesList[functionPropertyID].Quantity); break; - case FunctionCode.ReadDiscreteInputs: + case FunctionCodeRd.ReadDiscreteInputs: connectionProperties.FunctionPropertiesList[functionPropertyID].values = modbusClient.ReadDiscreteInputs(connectionProperties.FunctionPropertiesList[functionPropertyID].StartingAdress, connectionProperties.FunctionPropertiesList[functionPropertyID].Quantity); break; - case FunctionCode.ReadHoldingRegisters: + case FunctionCodeRd.ReadHoldingRegisters: connectionProperties.FunctionPropertiesList[functionPropertyID].values = modbusClient.ReadHoldingRegisters(connectionProperties.FunctionPropertiesList[functionPropertyID].StartingAdress, connectionProperties.FunctionPropertiesList[functionPropertyID].Quantity); break; - case FunctionCode.ReadInputRegisters: + case FunctionCodeRd.ReadInputRegisters: connectionProperties.FunctionPropertiesList[functionPropertyID].values = modbusClient.ReadInputRegisters(connectionProperties.FunctionPropertiesList[functionPropertyID].StartingAdress, connectionProperties.FunctionPropertiesList[functionPropertyID].Quantity); break; default: break; @@ -144,18 +161,18 @@ public void GetValues(ConnectionProperties connectionProperties) modbusClient.Connect(); } foreach (FunctionProperties functionProperty in connectionProperties.FunctionPropertiesList) - switch (functionProperty.FunctionCode) + switch (functionProperty.FunctionCodeRead) { - case FunctionCode.ReadCoils: + case FunctionCodeRd.ReadCoils: functionProperty.values = modbusClient.ReadCoils(functionProperty.StartingAdress, functionProperty.Quantity); break; - case FunctionCode.ReadDiscreteInputs: + case FunctionCodeRd.ReadDiscreteInputs: functionProperty.values = modbusClient.ReadDiscreteInputs(functionProperty.StartingAdress, functionProperty.Quantity); break; - case FunctionCode.ReadHoldingRegisters: + case FunctionCodeRd.ReadHoldingRegisters: functionProperty.values = modbusClient.ReadHoldingRegisters(functionProperty.StartingAdress, functionProperty.Quantity); break; - case FunctionCode.ReadInputRegisters: + case FunctionCodeRd.ReadInputRegisters: functionProperty.values = modbusClient.ReadInputRegisters(functionProperty.StartingAdress, functionProperty.Quantity); break; default: break; @@ -168,22 +185,22 @@ public void GetValues(ConnectionProperties connectionProperties) public event ConnectionPropertiesListChanged connectionPropertiesListChanged; - public static string getAddress(FunctionCode functionCode, int startingAddress, int quantity, int elementCount) + public static string getAddress(FunctionCodeRd functionCode, int startingAddress, int quantity, int elementCount) { string returnValue = null; if ((startingAddress + elementCount) <= (startingAddress + quantity)) switch (functionCode) { - case FunctionCode.ReadCoils: + case FunctionCodeRd.ReadCoils: returnValue = "0x" + (startingAddress + elementCount + 1).ToString(); break; - case FunctionCode.ReadDiscreteInputs: + case FunctionCodeRd.ReadDiscreteInputs: returnValue = "1x" + (startingAddress + elementCount + 1).ToString(); break; - case FunctionCode.ReadHoldingRegisters: + case FunctionCodeRd.ReadHoldingRegisters: returnValue = "4x" + (startingAddress + elementCount + 1).ToString(); break; - case FunctionCode.ReadInputRegisters: + case FunctionCodeRd.ReadInputRegisters: returnValue = "3x" + (startingAddress + elementCount + 1).ToString(); break; default: break; @@ -191,71 +208,138 @@ public static string getAddress(FunctionCode functionCode, int startingAddress, return returnValue; } + public static int[] StrToValues(FunctionProperties functionProperties, string str) + { + int[] values = { }; + switch (functionProperties.FunctionCodeWrite) + { + case FunctionCodeWr.WriteHoldingRegisters: + int value = 0; + if (Int32.TryParse(str, out value)) + { + // add + int[] x = { value }; + return x; + + } + + break; + } + return values; + } + + public FunctionProperties FindPropertyFromGrid( int gridRow) + { + foreach (ConnectionProperties connection in connectionPropertiesList) + { + foreach (FunctionProperties functionProperty in connection.FunctionPropertiesList) + { + if (functionProperty.DataGridRow == gridRow) + { + return functionProperty; + } + } + } + + return null; + } + + public void WriteToServer(FunctionProperties prop, int[] values) + { + /* + string text = ""; + text += "property " + prop.StartingAdress + "\n" + "type " + prop.FunctionCodeWrite.ToString() + "\n" + "new value: " + values.ToString() + "\n"; + text += "connection " + prop.Connection.ConnectionName; + MessageBox.Show(text, "updating register"); + */ + + int startingAddress = prop.StartingAdress; + switch(prop.FunctionCodeWrite) + { + case FunctionCodeWr.WriteHoldingRegisters: + prop.Connection.modbusClient.WriteMultipleRegisters(startingAddress, values); + break; + + } + + + } + public void WriteXML(DataGridView dataGridView) { XmlDocument xmlDocument = new XmlDocument(); XmlNode xmlRoot; - XmlNode xmlChild1; - XmlNode xmlChild2, xmlChild3; + XmlNode xmlNodeConnection, xmlNodeConnectionProp; + XmlNode xmlNodeFunctionCodes, xmlNodeFunctionCodesProp; + XmlNode xmlNodeDataGrid, xmlNodeDataGridLines, xmlNodeDataGridLinesProp; xmlRoot = xmlDocument.CreateElement("ModbusConfiguration"); for (int i = 0; i < this.connectionPropertiesList.Count; i++) { - xmlChild1 = xmlDocument.CreateElement("connection"); - xmlChild2 = xmlDocument.CreateElement("connectionName"); - xmlChild2.InnerText = this.connectionPropertiesList[i].ConnectionName; - xmlChild1.AppendChild(xmlChild2); - xmlChild2 = xmlDocument.CreateElement("ipAddress"); - xmlChild2.InnerText = this.connectionPropertiesList[i].ModbusTCPAddress; - xmlChild1.AppendChild(xmlChild2); - xmlChild2 = xmlDocument.CreateElement("port"); - xmlChild2.InnerText = this.connectionPropertiesList[i].Port.ToString(); - xmlChild1.AppendChild(xmlChild2); - xmlChild2 = xmlDocument.CreateElement("cyclicFlag"); - xmlChild2.InnerText = this.connectionPropertiesList[i].CyclicFlag.ToString(); - xmlChild1.AppendChild(xmlChild2); - xmlChild2 = xmlDocument.CreateElement("cycleTime"); - xmlChild2.InnerText = this.connectionPropertiesList[i].CycleTime.ToString(); - xmlChild1.AppendChild(xmlChild2); + xmlNodeConnection = xmlDocument.CreateElement("connection"); + xmlNodeConnectionProp = xmlDocument.CreateElement("connectionName"); + xmlNodeConnectionProp.InnerText = this.connectionPropertiesList[i].ConnectionName; + xmlNodeConnection.AppendChild(xmlNodeConnectionProp); + xmlNodeConnectionProp = xmlDocument.CreateElement("ipAddress"); + xmlNodeConnectionProp.InnerText = this.connectionPropertiesList[i].ModbusTCPAddress; + xmlNodeConnection.AppendChild(xmlNodeConnectionProp); + xmlNodeConnectionProp = xmlDocument.CreateElement("port"); + xmlNodeConnectionProp.InnerText = this.connectionPropertiesList[i].Port.ToString(); + xmlNodeConnection.AppendChild(xmlNodeConnectionProp); + xmlNodeConnectionProp = xmlDocument.CreateElement("cyclicFlag"); + xmlNodeConnectionProp.InnerText = this.connectionPropertiesList[i].CyclicFlag.ToString(); + xmlNodeConnection.AppendChild(xmlNodeConnectionProp); + xmlNodeConnectionProp = xmlDocument.CreateElement("cycleTime"); + xmlNodeConnectionProp.InnerText = this.connectionPropertiesList[i].CycleTime.ToString(); + xmlNodeConnection.AppendChild(xmlNodeConnectionProp); for (int j = 0; j < this.connectionPropertiesList[i].FunctionPropertiesList.Count; j++) { - xmlChild2 = xmlDocument.CreateElement("functionCodes"); - xmlChild3 = xmlDocument.CreateElement("functionCode"); - xmlChild3.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].FunctionCode.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild3 = xmlDocument.CreateElement("quantity"); - xmlChild3.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].Quantity.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild3 = xmlDocument.CreateElement("startingAddress"); - xmlChild3.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].StartingAdress.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild1.AppendChild(xmlChild2); + xmlNodeFunctionCodes = xmlDocument.CreateElement("functionCodes"); + xmlNodeFunctionCodesProp = xmlDocument.CreateElement("functionCodeRead"); + xmlNodeFunctionCodesProp.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].FunctionCodeRead.ToString(); + xmlNodeFunctionCodes.AppendChild(xmlNodeFunctionCodesProp); + xmlNodeFunctionCodesProp = xmlDocument.CreateElement("functionCodeWrite"); + xmlNodeFunctionCodesProp.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].FunctionCodeWrite.ToString(); + xmlNodeFunctionCodes.AppendChild(xmlNodeFunctionCodesProp); + xmlNodeFunctionCodesProp = xmlDocument.CreateElement("quantity"); + xmlNodeFunctionCodesProp.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].Quantity.ToString(); + xmlNodeFunctionCodes.AppendChild(xmlNodeFunctionCodesProp); + xmlNodeFunctionCodesProp = xmlDocument.CreateElement("startingAddress"); + xmlNodeFunctionCodesProp.InnerText = this.connectionPropertiesList[i].FunctionPropertiesList[j].StartingAdress.ToString(); + xmlNodeFunctionCodes.AppendChild(xmlNodeFunctionCodesProp); + xmlNodeConnection.AppendChild(xmlNodeFunctionCodes); } - xmlRoot.AppendChild(xmlChild1); - xmlChild1 = xmlDocument.CreateElement("dataGridView"); + xmlRoot.AppendChild(xmlNodeConnection); + xmlNodeDataGrid = xmlDocument.CreateElement("dataGridView"); for (int j = 0; j < dataGridView.Rows.Count; j++) { - if (dataGridView[0, j].Value != null & dataGridView[1, j].Value!= null & dataGridView[2, j].Value != null & dataGridView[3, j].Value != null) - xmlChild2 = xmlDocument.CreateElement("dataGridViewLines"); - xmlChild3 = xmlDocument.CreateElement("columnConnection"); - if (dataGridView[0, j].Value != null) - xmlChild3.InnerText = dataGridView[0, j].Value.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild3 = xmlDocument.CreateElement("columnAddress"); - if (dataGridView[1, j].Value != null) - xmlChild3.InnerText = dataGridView[1, j].Value.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild3 = xmlDocument.CreateElement("columnTag"); - if (dataGridView[2, j].Value != null) - xmlChild3.InnerText = dataGridView[2, j].Value.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild3 = xmlDocument.CreateElement("columnDataType"); - if (dataGridView[3, j].Value != null) - xmlChild3.InnerText = dataGridView[3, j].Value.ToString(); - xmlChild2.AppendChild(xmlChild3); - xmlChild1.AppendChild(xmlChild2); + if (dataGridView[0, j].Value != null & dataGridView[1, j].Value != null & dataGridView[3, j].Value != null) + { + xmlNodeDataGridLines = xmlDocument.CreateElement("dataGridViewLines"); + + xmlNodeDataGridLinesProp = xmlDocument.CreateElement("columnConnection"); + xmlNodeDataGridLinesProp.InnerText = dataGridView[0, j].Value.ToString(); + xmlNodeDataGridLines.AppendChild(xmlNodeDataGridLinesProp); + + xmlNodeDataGridLinesProp = xmlDocument.CreateElement("columnAddress"); + xmlNodeDataGridLinesProp.InnerText = dataGridView[1, j].Value.ToString(); + xmlNodeDataGridLines.AppendChild(xmlNodeDataGridLinesProp); + + xmlNodeDataGridLinesProp = xmlDocument.CreateElement("columnTag"); + if (dataGridView[2, j].Value != null) + xmlNodeDataGridLinesProp.InnerText = dataGridView[2, j].Value.ToString(); + else + xmlNodeDataGridLinesProp.InnerText = "n.a."; + xmlNodeDataGridLines.AppendChild(xmlNodeDataGridLinesProp); + + xmlNodeDataGridLinesProp = xmlDocument.CreateElement("columnDataType"); + xmlNodeDataGridLinesProp.InnerText = dataGridView[3, j].Value.ToString(); + xmlNodeDataGridLines.AppendChild(xmlNodeDataGridLinesProp); + xmlNodeDataGrid.AppendChild(xmlNodeDataGridLines); + } } - xmlRoot.AppendChild(xmlChild1); + xmlRoot.AppendChild(xmlNodeDataGrid); + xmlDocument.AppendChild(xmlRoot); xmlDocument.Save("textWriter.xml"); } @@ -273,10 +357,15 @@ public void ReadXML(DataGridView dataGridView) System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); xmlDocument.Load("textWriter.xml"); xmlNodeList = xmlDocument.GetElementsByTagName("connection"); - connectionPropertiesList = new List(); + //connectionPropertiesList = new List(); + this.connectionPropertiesList.Clear(); + int slotId = 0; + foreach (XmlNode xmlNode in xmlNodeList) { ConnectionProperties connectionProperty = new ConnectionProperties(); + AddConnection(connectionProperty); + connectionProperty.ConnectionName = (xmlNode["connectionName"].InnerText); connectionProperty.ModbusTCPAddress = (xmlNode["ipAddress"].InnerText); connectionProperty.Port = Int32.Parse(xmlNode["port"].InnerText); @@ -287,36 +376,61 @@ public void ReadXML(DataGridView dataGridView) { xmlNodeList2 = xmlNode3.ChildNodes; FunctionProperties functionProperty = new FunctionProperties(); + foreach (XmlNode xmlNode2 in xmlNodeList2) { - if (xmlNode2.Name == "functionCode") + if (xmlNode2.Name == "functionCodeRead") switch (xmlNode2.InnerText) { case "ReadCoils": - functionProperty.FunctionCode = FunctionCode.ReadCoils; + functionProperty.FunctionCodeRead = FunctionCodeRd.ReadCoils; break; case "ReadDiscreteInputs": - functionProperty.FunctionCode = FunctionCode.ReadDiscreteInputs; + functionProperty.FunctionCodeRead = FunctionCodeRd.ReadDiscreteInputs; break; case "ReadHoldingRegisters": - functionProperty.FunctionCode = FunctionCode.ReadHoldingRegisters; + functionProperty.FunctionCodeRead = FunctionCodeRd.ReadHoldingRegisters; break; case "ReadInputRegisters": - functionProperty.FunctionCode = FunctionCode.ReadInputRegisters; + functionProperty.FunctionCodeRead = FunctionCodeRd.ReadInputRegisters; break; } + if (xmlNode2.Name == "functionCodeWrite") + { + functionProperty.FunctionCodeWrite = FunctionCodeWr.WriteNone; + switch (xmlNode2.InnerText) + { + case "WriteCoils": + functionProperty.FunctionCodeWrite = FunctionCodeWr.WriteNone; + break; + case "WriteDiscreteInputs": + functionProperty.FunctionCodeWrite = FunctionCodeWr.WriteNone; + break; + case "WriteHoldingRegisters": + functionProperty.FunctionCodeWrite = FunctionCodeWr.WriteHoldingRegisters; + break; + case "WriteInputRegisters": + functionProperty.FunctionCodeWrite = FunctionCodeWr.WriteNone; + break; + } + + } if (xmlNode2.Name == "startingAddress") functionProperty.StartingAdress = Int32.Parse(xmlNode2.InnerText); if (xmlNode2.Name == "quantity") functionProperty.Quantity = Int32.Parse(xmlNode2.InnerText); } - connectionProperty.FunctionPropertiesList.Add(functionProperty); + //connectionProperty.FunctionPropertiesList.Add(functionProperty); + this.AddFunctionProperty(functionProperty, slotId); + xmlNode3 = xmlNode3.NextSibling; } - connectionPropertiesList.Add(connectionProperty); + slotId++; + //this.connectionPropertiesList.Add(connectionProperty); } if (connectionPropertiesListChanged != null) connectionPropertiesListChanged(this); + xmlNodeList = xmlDocument.GetElementsByTagName("dataGridViewLines"); dataGridView.Rows.Clear(); dataGridView.AllowUserToAddRows = false; @@ -337,37 +451,59 @@ public void ReadXML(DataGridView dataGridView) if (xmlNode["columnDataType"] != null) dataGridView[3, dataGridView.Rows.Count - 1].Value = xmlNode["columnDataType"].InnerText; } + + // trigger update of values manually + //this.valuesChanged(this); + dataGridView.AllowUserToAddRows = true; } } - public enum FunctionCode : int + public enum FunctionCodeRd : int { ReadCoils = 1, ReadDiscreteInputs = 2, ReadHoldingRegisters = 3, ReadInputRegisters = 4, }; - - - public class FunctionProperties + + public enum FunctionCodeWr : int + { + WriteNone = 0, + WriteCoils = 1, + WriteDiscreteInputs = 2, + WriteHoldingRegisters = 3, + WriteInputRegisters = 4, + }; + + + public class FunctionProperties { - FunctionCode funtionCode = FunctionCode.ReadCoils; - - [Browsable(true)] + FunctionCodeRd functionCodeRd = FunctionCodeRd.ReadCoils; + [Browsable(true)] [Category("Function code properties")] - [Description("Function Code")] - [DisplayName("Function Code")] - public FunctionCode FunctionCode + [Description("Function Code Read")] + [DisplayName("Function Code Read")] + public FunctionCodeRd FunctionCodeRead { - get {return funtionCode;} - set {funtionCode = value;} + get {return functionCodeRd; } + set { functionCodeRd = value;} } - - - int startingAdress = 0; + + FunctionCodeWr functionCodeWr = FunctionCodeWr.WriteNone; + [Browsable(true)] + [Category("Function code properties")] + [Description("Function Code Write")] + [DisplayName("Function Code Write")] + public FunctionCodeWr FunctionCodeWrite + { + get { return functionCodeWr; } + set { functionCodeWr = value; } + } + + int startingAdress = 0; [Browsable(true)] [Category("Function code properties")] [Description("Starting Address")] @@ -389,7 +525,29 @@ public int Quantity get {return quantity;} set {quantity = value;} } - - public object values; + + int DataGridRowIdx = -1; + [Browsable(false)] + [Category("Function code properties")] + [Description("Data Grid Row Idx")] + [DisplayName("Data Grid Row Idx")] + public int DataGridRow + { + get { return DataGridRowIdx; } + set { DataGridRowIdx = value; } + } + + ConnectionProperties connection= null; + [Browsable(false)] + [Category("Function code properties")] + [Description("connection")] + [DisplayName("connection")] + public ConnectionProperties Connection + { + get { return connection; } + set { connection = value; } + } + + public object values; } } diff --git a/EasyModbusAdvancedClient/MainForm.Designer.cs b/EasyModbusAdvancedClient/MainForm.Designer.cs index 4cf5860..b33260c 100644 --- a/EasyModbusAdvancedClient/MainForm.Designer.cs +++ b/EasyModbusAdvancedClient/MainForm.Designer.cs @@ -416,6 +416,7 @@ private void InitializeComponent() this.dataGridView1.Size = new System.Drawing.Size(766, 662); this.dataGridView1.TabIndex = 0; this.dataGridView1.CellClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1CellClick); + this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit); // // Column5 // @@ -447,7 +448,8 @@ private void InitializeComponent() "UINT16 (0...65535)", "BOOL (FALSE...TRUE)", "INT16 (-32768...32767)", - "WORD16 (0...65535)"}); + "WORD16 (0...65535)", + "ASCII"}); this.Column4.Name = "Column4"; this.Column4.Resizable = System.Windows.Forms.DataGridViewTriState.True; this.Column4.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; diff --git a/EasyModbusAdvancedClient/MainForm.cs b/EasyModbusAdvancedClient/MainForm.cs index 2f26e74..98e5a65 100644 --- a/EasyModbusAdvancedClient/MainForm.cs +++ b/EasyModbusAdvancedClient/MainForm.cs @@ -117,7 +117,7 @@ private void UpdateListBox(object sender) treeNode = new TreeNode("Modbus-RTU Connection: " + connectionProperty.ConnectionName +"; COM-Port: "+connectionProperty.ComPort); foreach (FunctionProperties functionProperty in connectionProperty.FunctionPropertiesList) { - treeNode.Nodes.Add("Function code: " + functionProperty.FunctionCode + "; Starting Address: "+functionProperty.StartingAdress + "; Quantity: "+functionProperty.Quantity); + treeNode.Nodes.Add("Function code: " + functionProperty.FunctionCodeRead + "; Starting Address: "+functionProperty.StartingAdress + "; Quantity: "+functionProperty.Quantity); } rootNode.Nodes.Add(treeNode); @@ -138,24 +138,38 @@ private void UpdateDataGridView(object sender) { foreach (FunctionProperties functionProperty in connectionProperty.FunctionPropertiesList) { + if (dataGridView1[1,i].Value != null) - for (int j = 0; j < functionProperty.Quantity; j++) - if (EasyModbusManager.getAddress(functionProperty.FunctionCode, functionProperty.StartingAdress, functionProperty.Quantity, j).Equals(dataGridView1[1,i].Value.ToString())) + + for (int j = 0; j < functionProperty.Quantity; j++) + if (EasyModbusManager.getAddress(functionProperty.FunctionCodeRead, functionProperty.StartingAdress, functionProperty.Quantity, j).Equals(dataGridView1[1,i].Value.ToString())) { - if (functionProperty.values.GetType().Equals(typeof(Boolean[]))) + functionProperty.DataGridRow = i; + if (functionProperty.values.GetType().Equals(typeof(Boolean[]))) dataGridView1[4,i].Value=((bool[]) functionProperty.values)[j].ToString(); else { - if (dataGridView1[3,i].Value != null) - if (dataGridView1[3,i].Value.Equals("UINT16 (0...65535)")) - if (((int[]) functionProperty.values)[j] < 0) - dataGridView1[4,i].Value=(65536+((int[]) functionProperty.values)[j]).ToString(); - else - dataGridView1[4,i].Value=((int[]) functionProperty.values)[j].ToString(); - else - dataGridView1[4,i].Value=((int[]) functionProperty.values)[j].ToString(); - else - dataGridView1[4,i].Value=((int[]) functionProperty.values)[j].ToString(); + if (dataGridView1[3, i].Value != null) + if (dataGridView1[3, i].Value.Equals("UINT16 (0...65535)")) + if (((int[])functionProperty.values)[j] < 0) + dataGridView1[4, i].Value = (65536 + ((int[])functionProperty.values)[j]).ToString(); + else + dataGridView1[4, i].Value = ((int[])functionProperty.values)[j].ToString(); + else if (dataGridView1[3, i].Value.Equals("ASCII")) + { + + string str = ""; + for (int tt = 0; tt < ((int[])functionProperty.values).Length; tt++) + { + int value = ((int[])functionProperty.values)[tt]; + str += "" + (char)((value & 0xff00) >> 8) + (char)((value & 0x00ff)); + } + dataGridView1[4, i].Value = "" + str; + } + else + dataGridView1[4, i].Value = ((int[])functionProperty.values)[j].ToString(); + else + dataGridView1[4, i].Value = ((int[])functionProperty.values)[j].ToString(); } } } @@ -341,15 +355,15 @@ void Button2Click(object sender, EventArgs e) DataGridView1CellClick(null, null); // DataGridViewComboBoxCell cbCell = (DataGridViewComboBoxCell)dataGridView1.Rows[row].Cells[1]; - switch (functionProperty.FunctionCode) + switch (functionProperty.FunctionCodeRead) { - case FunctionCode.ReadCoils: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "0x"+(functionProperty.StartingAdress+i+1).ToString(); + case FunctionCodeRd.ReadCoils: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "0x"+(functionProperty.StartingAdress+i+1).ToString(); break; - case FunctionCode.ReadDiscreteInputs: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "1x"+(functionProperty.StartingAdress+i+1).ToString(); + case FunctionCodeRd.ReadDiscreteInputs: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "1x"+(functionProperty.StartingAdress+i+1).ToString(); break; - case FunctionCode.ReadHoldingRegisters: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "4x"+(functionProperty.StartingAdress+i+1).ToString(); + case FunctionCodeRd.ReadHoldingRegisters: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "4x"+(functionProperty.StartingAdress+i+1).ToString(); break; - case FunctionCode.ReadInputRegisters: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "3x"+(functionProperty.StartingAdress+i+1).ToString(); + case FunctionCodeRd.ReadInputRegisters: dataGridView1[1,dataGridView1.Rows.Count-1].Value = "3x"+(functionProperty.StartingAdress+i+1).ToString(); break; default: break; @@ -429,19 +443,19 @@ void DataGridView1CellClick(object sender, DataGridViewCellEventArgs e) for (int l = 0; l < easyModbusManager.connectionPropertiesList[j].FunctionPropertiesList[k].Quantity; l++) { currentAddress = easyModbusManager.connectionPropertiesList[j].FunctionPropertiesList[k].StartingAdress + l + 1; - switch (easyModbusManager.connectionPropertiesList[j].FunctionPropertiesList[k].FunctionCode) + switch (easyModbusManager.connectionPropertiesList[j].FunctionPropertiesList[k].FunctionCodeRead) { - case FunctionCode.ReadCoils: + case FunctionCodeRd.ReadCoils: cbCell.Items.Add("0x" + currentAddress.ToString()); break; - case FunctionCode.ReadDiscreteInputs: + case FunctionCodeRd.ReadDiscreteInputs: cbCell.Items.Add("1x" + currentAddress.ToString()); break; - case FunctionCode.ReadHoldingRegisters: + case FunctionCodeRd.ReadHoldingRegisters: cbCell.Items.Add("4x" + currentAddress.ToString()); break; - case FunctionCode.ReadInputRegisters: + case FunctionCodeRd.ReadInputRegisters: cbCell.Items.Add("3x" + currentAddress.ToString()); break; default: break; @@ -475,6 +489,7 @@ void DataGridView1CellClick(object sender, DataGridViewCellEventArgs e) { cbCell.Items.Add("INT16 (-32768...32767)"); cbCell.Items.Add("UINT16 (0...65535)"); + cbCell.Items.Add("ASCII"); } if (selectedCell != null) { @@ -752,6 +767,23 @@ void UpdateTextBoxSend(object sender) textBox1.AppendText(System.Environment.NewLine); } } + + private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) + { + // check if we are in column value + if (e.ColumnIndex == 4) + { + int idx = e.RowIndex; + FunctionProperties functionProperties= easyModbusManager.FindPropertyFromGrid(idx); + if (functionProperties != null) + { + string str = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString(); + int[] values = EasyModbusManager.StrToValues(functionProperties, str); + easyModbusManager.WriteToServer(functionProperties, values); + } + } + + } } } diff --git a/EasyModbusAdvancedClient/MainForm.resx b/EasyModbusAdvancedClient/MainForm.resx index d375074..755aea9 100644 --- a/EasyModbusAdvancedClient/MainForm.resx +++ b/EasyModbusAdvancedClient/MainForm.resx @@ -120,240 +120,210 @@ - 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 - sdoDyf8Abx8BU8j+94oAAAAASUVORK5CYII= + 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 + /wBvHwFTyP73igAAAABJRU5ErkJggg== - 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 - 99wFhgAAAABJRU5ErkJggg== + iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAABZ0RVh0U291 + cmNlAENyeXN0YWwgUHJvamVjdOvj5IsAAAk9SURBVFhHvZYJUFPXGsdvEkPDkgQCwSyyQwhbgiRA2HcQ + UbSAotK6b33KUhERcWWzKovLUwFbQAGxIC48RYuo5RW1LbZWqdqqT1pr26dttZZKKyb590QnGd8Yp2/G + Tifzm3vmO98532/Od+/NpQD8pcikzpmB/op6Y3PGMBp8GdJHcpsv9vVC4SXZZk6jaMZynsVo8GWYKWI3 + ae7fxNefnUakwn0vn6IYxvL0GA2+DAvtzJseXDgEDLyP270tiJU6NtrOZtCN5eowGnwZsuxZjXe7tuDn + 09sx9MFOfLmvDFOUVidt6lhP2uGzwYkmWMA2tMawMEkhZqaqxPwkCcWfJqfz5ynp/OwgOj8/jM5fE0Xn + l8bS+Rvj6fyKBLptZSJdoKNiDEOwKYEheCueISiOZQiWh1KuJapR3bdbl+P2vqX4ujkH/21dgr7K+Zjk + w+7S15KWi54XmBxiV9YyJR4tqaGafWmhmnfTgjWtk4I1+yerNO3pKs2BKQGag1P9dWgPTVUSFNpD6b7a + g5Pl2vY0mbbtVR9ty3gv7f5UJW7VZeDLban4vHIiPtuYhCsV49C1LAHj3VjH9fX0GAYzQ/m7fz1RikfH + CjHUuQy/H8/F4+5saN5fBHywEDgzFzhLOEfGOnrnAD3TgO6JwNFEPGqPwy/NMbhbG4KBqgBcKfXD+UI5 + epZ4oTtTgp5sCZpflyJNYnHUqEBmlHX9d41z8N2OFHy7Mxk/1CXhQXMChtpioT4YCRwhHE8CTqQR0oGu + ScCx8cDhCOBdGdS7nfB7jRD3N9vi2/U2+KLQEn3ZbJyYY4796SzsTWGiLXUE/pnEx6vupu3PCSyPt6q/ + XhWPa2tH42aZD26Xe+CHra54WOsA7W47UkROisUAna8C700lZBCBKUSMnMABEm/2BXYJoKkagQclFL5e + QeFiNoVTcyi8O4lCdSKFmkQaNo2xxbxRTMMpGASKxnLqLxR64FI2F9cKLPBplinZxByPyy2BGinQkkiO + eh7w/krSgnKgbztwfie5biPteYu0Io8IvgbUB0JbZo6f8il8vpjC8ekUapMprIqgoyCKj4X2Jp36mv8j + sCmZXf9hrgCfLaJwbRkhi4ahai9cW654uD+M91NHnODekYRR944mOd87miwhSJ8yXnLvSJLrvY5Ep3sH + YsQ/npruP6TdpQJKLfAT2eeTBRQOptOwfQIf853px54trsMw2J7KbvhwmRD9OU+P7+cCOh7XB2CnM/Ok + bv5RHsUaLqRYKCCsobOwjsHCWhoLqyiWmsQekHld3i4Js1ezn9yc1b5Ql5jim+UjcGqRCG/6sJ7sI9zN + M5e2CU31dQ0C76Szaz8qEOFqLjV0dzU1pC5lD2n2xqLWiXZYn/P/0ODJPK09WQjsS4FmhxuuV7hgVTj/ + bMANudDkAOUubuR5EgFyUz3NNyxsymAX9a0QDVxfRvX/uo7qx2bHfnJ2X1Xb09t08+GfRqdItzi/o89/ + EY3erNM4X02ekjz8+HYksqP4A29oZkWornglEYFx4ibeBPdWAenR03zDwrbp7HnnC0XdA/lUx3AJvQM1 + ig50zu+uEDN3R55zD5/VmYHIpoBz+vwX0SjnnMZX3eSpmavZ5G/6kctGx87MoRlLlJckuUQgV9xkvdxt + n+1kfb5h4aFZ7BQisOtWPrVVvZ61FQ3RW9G1tKJkJv9QxOmI37rQgdjDYb36/BexR8btediahxon+uH5 + 52Ri5zKn6wt+nrJb9olTg8l+qkG8x7rFuck6X59vWPiv2ezoj1eIVn6zjMrTrrfIw56x/+g4kdEc2qnQ + vom5qEUVQtuVfyrQHsArrnN45YhuHPyfEJlnuev9GXcmfu5+VtTP3Ef1i+qsbzjV8QytNCw8OpetOJcv + mnErl5qGMs74w3Xyzd573TTJt2Iw85eJWIUc+LV6/qnAs6j6ZYFeVRJMGojTOpy01ozYQ2mEO3hw2sF7 + 8kToMCQTAbeeXFHcnSxKcbDYbKXdVlu1T48zoi8pkHY7DgsfTYXX2059PkftmQEfuXOCP/XmhvfLuVGX + R3Ojryi40VcV3KAvCP2+XOVFGde3x43p3+CS5lUuQUJ/IGw7LMCopSAs58GxgnfqOYEjc9gje7MFdlUL + GItsV3Afs+sZcOywhPKMG+Iu+yP9zhgkN4arZZXSQWW1fDCo2n8wrCZoMKImdDCiNmQwpEY1qKpRDvqR + OdkOj0HPLW6DERsUv4XXKaE6IwW7hQl6JQXBKh4cio0I6CiYK5juluU4zMvlwKb8FYj2suFxTIwAskFM + vxJTv0/EoocZyFPPRxFyUY412IISbEYxNqCQ/DKRPTwDs++nYPLteCReD0bgWSmcO/gwqaaBsZIIvMmD + /QojAtPiWcneOd7quI0JiCwKR1h5KILIe111UIHA46Oh+rccgee9EXDZE/433KG85QK/7x3he8cePnfF + 8Lw7EpJvyfEOcCG6Ygabj5mwPMmAeTsdJm/TMKKYBjp5LQsyebBbYvW8QGwIa6rnYm+MqxqH2NIYRJVE + IKySSDQQiXYlAjpHw/+UDxRnPOHXJ4HvRRfIvnCA941R8LgpgOQmHy7XrWB/lQPhBTNY9zLBeY8Bs1Y6 + mNuJwBIiQP6YBFk8jHrDiICOCQk2s90XeWBs5VhEl0QiojgMIZuCoKr1h3+LH/wOyaDsksOvxxt+H3vA + 95Ib5Fed4X3NAdJrYrh9ORJOl20gvsAF/6w5uF0mMG8bAZMdDJisZYCRTYdwqQ2Er79AQEecP2O+R5YX + xlSOQWQJaUVRCIJKAxGwWQGPUnetsECgEa4cqRautlUL1xGKCCV8tYBgW2Kt5hfz1Px1Vmrr1ZZqXiFH + zc3nDHNz2cOcbPYwL8fqkddKd8hnSpv19QyFt6XyaGvSnJ98LE6IMH/DK8cH8RVxCC8JRcg6cgrrAsFf + zPuKs5Rmx1lNc+EW0V0tywhvMVytNjzFcj25lhDWkvEKQh7DlZvJcODMZdhbZDDszSbQ7bkJLEdxAp/9 + nMCaUBPalhQXw9fqrATTTK8cGWIrYhBaHIzgYhVMpjOv6Of/KowG9ZhJzXLkuTLEVEQhZEMwWK+Z/L0C + OtiuFkvl2T6IronEK+l/8wnoMbVlzfbNk8EqmXvW2PzLYDRoDEdXx8XisaInHyd/HaD+AEkql2H33AWG + AAAAAElFTkSuQmCC - iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAAK/INwWK6QAAAAZiS0dEAAAA - AAAA+UO7fwAAAAl2cEFnAAAAIAAAACAAh/qcnQAAACV0RVh0Y3JlYXRlLWRhdGUAMjAwOS0xMi0wOFQx - Mjo1MzoxMy0wNzowMJ9lzD0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTAtMDItMjBUMjM6MjY6MTctMDc6 - MDCRpE2oAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDEwLTAxLTExVDA5OjIxOjA1LTA3OjAw3IUR7QAAADV0 - RVh0TGljZW5zZQBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9MR1BMLzIuMS87wbQY - AAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTEyLTA4VDEyOjUzOjEzLTA3OjAwwNS6CQAAABl0RVh0U29m - dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAANdEVYdFNvdXJjZQBOdXZvbGGsTzXxAAAANHRFWHRT - b3VyY2VfVVJMAGh0dHA6Ly93d3cuaWNvbi1raW5nLmNvbS9wcm9qZWN0cy9udXZvbGEvdj20UgAABpZJ - REFUWEftlntQU1cexw8hAQnCFgoxiyiZBUSoIlhRSgsKug2GV0WoUMWK4y46W2vrjlK0OpYUEQxPQSwC - usW3oDxUKALyUB6VyjYbEKlOt2McO7shtD5QgfDt795md9qZqnTqH/3D78xnzr0nub/f955zfudc9lzP - NR4JBCbMwcGBzZo1i/n6+jI/Pz/20kszma2tPTM3t2Q2NvbM29vf+O9nLxPCQyAQJIrFFvstLS1LxGJx - gUhk9iFZi6TfZBYWlqa2thLm7/86/8CzFJc8UCqVdi1Z8ubI2rXvYccOJbKzs6n9aDQm5q277u4z1DQC - W6ytX5w6aZIjc3Ka9uOTz0jTJRJp5+bN29HaehmXL/egpqYdN25owWlwcJD6W7FpU+KIu7tnvb395AUy - mauJh8fLxsfHKewhcohMIoPo4rvNRSKRKjb2bUNj4yX09fVBq72J7u5elJU1Qqf7jjfBaWhoCIcOHYG/ - f5DG0dEl0NNznklExDI+yLiErcR+EUOThJI7MpTbc93Tpzi5avYVHsWF5nZcvvIl1Jpe9Pf3o66uDe3t - /8Lo6ChhgMFgwPDwMI4fL8PcuQEXXVxmuPOBxyskMwH2mfqg3Holqm1W4MwfYg68MyE9ZY3X/c6jifiy - LAnXTm2H+lwxOjo6celSJ86cuYiBge/w8OEwHjx4xLf37g0hNVU15uXlm+nm5mVmDP904e9koMB0K5qk - Blx1HkW306ihfbLB0CTFWK0dxipsMHbYAro8H9RXnUZ9wwUy0Irr129S0ge4c2eIuE9GhtHTcxVRUXHX - XF293Izhny6AyDCZi5M2N6B2Br6QAW2TgQYJcMYWKLMCjkzAyC4rXNqdiJOnKnDuXAvU6uvQ6+/SSHDc - 4a8HB+9i166shz4+CxKcnWcZMzxF+DPxCbP4PklUcCPeStcfZzXwzxjrkasrJehdboeeGFv0RL8ATYgV - apcvRlFREc6ebaAFeQ23bw8Q+v+j19/D0aOnxwICFFkTJ9pwZfx0IY1IMWVNQUyaa8ZeXsmYPNDF7Xpx - chryt2xDxsZNSPnbeiT/NQHpSUnIy8tDZWUNlWUvTYOWyvIWD3et1epQXV0HuXzpEV9fuciY4smi0hMg - XzAJxSInHBE5KiMEsxWvefadPlyIw4UqlOR+jL1p27A7VYnMrGzk5ubyBjo6NPw0qNU0c0a++uoWGTjP - Gai0s3OwNqZ4sqBkE1Fk9inqJVq0SP89Umd/89tjkke3D8mgLZqC/xROhj5bggvKCKgysmgnzKIkn6Gt - Tc2b+B/t7RpoNF9TOVZh5kwfPYWmXYU58EmeJGxmQuwTbUO74xi+cQOu/gnonoKRFglGP6NFWG0NnDCD - 9mMX7ElJRiZtxXV1zbQLdqOhoetnXLnSj5ycIoNMNu0OhR4mThDOXJ7HComEinnfz5jYr9s7CbocO+hV - ttCnWkO3wwIDW80x8IEQ3749AYfXxSE7Lx8tLZ/j/PkOVFVdpNH4kZqaDjQ3d2HDhm3fTJ06bRWFbiDG - iAPEC8TjhVI2oXWBIP6kkzCt3Em4Uyk1O5X26uxHZ9fGYX/4QhQo5qNg8XyoEtaguOQgDXc3yssbcOzY - eRryer6tq/sce/eWIjIyvjgm5l1zChtA3CIGiRDi8UIK3whEjAmpFQrFkjnykMj+ppY27KeEHylTsCNZ - iZSdO6kEa9HY2ImSkiqiGsXFVWSgns6DasTHb+hVKN7ypSOCi2dJHCe4UdjOdTxWaGUszZaxRKrcD+he - OtVV7O7u/cmePQVobmmBSqVCcnIyDh78B7q61CgtraZqOEElWUb7AmeiHOvWJd4ODV0eGx29xvT991O5 - sPQ+/ELk3HDt+OXq5slksumz5fIITXl5BS24i6itraXNR42Kigbs3l2K9PRSMvYpfRvkYPXq9zRhYSti - o6LWCGkKjFEYV4aVhIFI4jrGLVNTSzZv3nyBh8ecN+ho7cvPL6RjuBKZmYXYuDEV69crkZCw5dGqVRtu - RUauOhASEjsnOvovgvDwOGME/mMmlPgvoSMWEb9OdKqxgIDFAk/PV+b4+S3KCgoKO7dw4ZKa4OBl1QrF - smKFIuad4ODo2cHBb4rpnpEB45NMQNAGz3qIh0Q6ISZ+vby9X2U+PvPZjBmvmPr5vW4dGBhms2jRG1ZB - QaHCpUtXs7Cw5Sw8fMVPk3PiDAQTVQR97rA/Er9NNBIsMDCUUXIml0cx7o1pzo2//qK4KbAiLPi753qu - 36cY+wG9v7AY/mc1iQAAAABJRU5ErkJggg== + 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 + Ab2/sBj+ZzWJAAAAAElFTkSuQmCC - iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAAK/INwWK6QAAAAZiS0dEAAAA - AAAA+UO7fwAAAAl2cEFnAAAAIAAAACAAh/qcnQAAACV0RVh0Y3JlYXRlLWRhdGUAMjAwOS0xMi0wOFQx - Mjo1MzoxMy0wNzowMJ9lzD0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTAtMDItMjBUMjM6MjY6MTctMDc6 - MDCRpE2oAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDEwLTAxLTExVDA5OjIxOjA4LTA3OjAwvVJwLQAAADV0 - RVh0TGljZW5zZQBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9MR1BMLzIuMS87wbQY - AAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTEyLTA4VDEyOjUzOjEzLTA3OjAwwNS6CQAAABl0RVh0U29m - dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAANdEVYdFNvdXJjZQBOdXZvbGGsTzXxAAAANHRFWHRT - b3VyY2VfVVJMAGh0dHA6Ly93d3cuaWNvbi1raW5nLmNvbS9wcm9qZWN0cy9udXZvbGEvdj20UgAACTxJ - REFUWEftln1Q00caxzeJ5BWQ8H5AMCiGVyEkKkoQxVFEXkSltb4V9bBateKJICgeYEAErKiAgIgop0hV - EI6qbdVSa2s9bcGq1QP1FMGGU4gvAZS38O0mpDdzvZuxdvpnvzPfmZ3d/e3zmWef3f2RP/SHfo2YTCZx - cHAgUqmU+Pr6kokT/Yi7uwexsLAmPJ4xMTe3JgpFiGH27y8GtTuTydjE43H38/n8UuoCIyP2ZjoUQcdE - fL4J08rKlkRFrdV/8HuKSR0mEjleW7hwSX98fBLS07dj9+7dSE5OHVi8OKpTKpV/Z2ZmGS8UWovt7ByJ - o+PooS9/J3nY24tuKJXZqK+/hXv3HuHWrYdQqzXQSaPR4NKlbxAT85d+icTjrI2Nw3iRyEmXsTcT8qj3 - UOdQ76T+Rt/N5XA4u6KjV2vr6i6hsbERra0tuHu3mcI0obOzWw+hU1dXF8rKDkOhmNLg4OA8SSLx1i/w - q4VE6v1GBBdtCOodCGqsdd3ejmLJnaJ9R1F34RK+bbiO6zdvoampCd99d1ufjf7+AWotBga06OnpxZEj - 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 @@ -361,358 +331,314 @@ - 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 - 36cY+wG9v7AY/mc1iQAAAABJRU5ErkJggg== + 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 + Ab2/sBj+ZzWJAAAAAElFTkSuQmCC - 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 - 99wFhgAAAABJRU5ErkJggg== + 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 + AAAAAElFTkSuQmCC - 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 - sdoDyf8Abx8BU8j+94oAAAAASUVORK5CYII= + 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 + /wBvHwFTyP73igAAAABJRU5ErkJggg== - iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAAA3QAAAN0AQIrQUUAAAAGYktHRAAAAAAAAPlDu38AAAAJdnBBZwAAACAAAAAgAIf6 - nJ0AAAAldEVYdGNyZWF0ZS1kYXRlADIwMDktMTEtMTVUMTc6MDI6MzctMDc6MDCHD5SPAAAAJXRFWHRk - YXRlOmNyZWF0ZQAyMDEwLTAyLTIwVDIzOjI2OjE4LTA3OjAwZ+w9QQAAACV0RVh0ZGF0ZTptb2RpZnkA - MjAxMC0wMS0xMVQwOTozMDoxMy0wNzowMF2zkLcAAABndEVYdExpY2Vuc2UAaHR0cDovL2NyZWF0aXZl - Y29tbW9ucy5vcmcvbGljZW5zZXMvYnktc2EvMy4wLyBvciBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9y - Zy9saWNlbnNlcy9MR1BMLzIuMS9bjzxjAAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTAzLTE5VDEwOjUy - OjQ4LTA2OjAwJlq4SwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAATdEVYdFNv - dXJjZQBPeHlnZW4gSWNvbnPsGK7oAAAAJ3RFWHRTb3VyY2VfVVJMAGh0dHA6Ly93d3cub3h5Z2VuLWlj - b25zLm9yZy/vN6rLAAAIzElEQVRYR72XC1BU1xnHz+7dx929+37CLvKWpzyCREziCNokgkRcRFRAEXwx - IFLwhYgy4INan6CgaKwBRVFQNMYmRE111MRO2yTVJm2aqU6SiZk0rU5am8bG6r/fvYDhsSbtZKY787u7 - 97v3fP//+c6559xlAH4QDEzmLf7f4jXoDVbLFGwpm2xdbT2UuDnxg1n7Z92pPV37oOli04PKk5V/z96f - fTNhc0K3cZUxi+5VecvhDa/BgbASpmPL2EaunPsyfU86anpq0PJ2Cw7+/iC6bnah408daL7WjNpLtVhy - agmmtkyFYoniLitnO8iIwVvO8R3j/dZcWZMo/h52cSBcFStULOf+ktOWgz3v7EHDtQZseXfLdyLes+NX - OzCxcSK4Mvkd+TJWMjCnol6+Pu9ULqaeyrgjng8S7Iecy9XlXEvU+ghsuLgB9b+sx/qr6/8nNlzdgNJX - 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 - SUVORK5CYII= + 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 + B8UAAAAASUVORK5CYII= - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAABSgAAAUoAQg00hkAAAAGYktHRAAAAAAAAPlDu38AAAAJdnBBZwAAADAAAAAwAM7u - jFcAAAAldEVYdGNyZWF0ZS1kYXRlADIwMDktMTEtMTVUMTc6MDI6MzUtMDc6MDAQkIWmAAAAJXRFWHRk - YXRlOmNyZWF0ZQAyMDEwLTAyLTIwVDIzOjI2OjE1LTA3OjAwBjtcgQAAACV0RVh0ZGF0ZTptb2RpZnkA - MjAxMC0wMS0xMVQwOTozMToyNS0wNzowMF8uyVAAAABndEVYdExpY2Vuc2UAaHR0cDovL2NyZWF0aXZl - Y29tbW9ucy5vcmcvbGljZW5zZXMvYnktc2EvMy4wLyBvciBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9y - Zy9saWNlbnNlcy9MR1BMLzIuMS9bjzxjAAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTAzLTE5VDEwOjUz - OjA1LTA2OjAwLAW8TwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAATdEVYdFNv - dXJjZQBPeHlnZW4gSWNvbnPsGK7oAAAAJ3RFWHRTb3VyY2VfVVJMAGh0dHA6Ly93d3cub3h5Z2VuLWlj - b25zLm9yZy/vN6rLAAAMNUlEQVRoQ9WZaVDV5xXGX+7lQq7GBVwRAUVREMQNxTUhLrjEHVQERRE1ikJU - FBUFkR25ctlBEGS59wKiYkSjWWxiY5qlbbpmmkxnOk1n+jGdttNPSWaePoclGnxjY0M+8OE3wuHMvec5 - 5znv//2PCsCARhscSGiDAwltcCChDQpqnXphaerS6+ZI8ybd3/vyrPn9hT64WrmyoC9y23Phudvza7VW - WVSGMuhyhSfyX1ZFT8vvT/TBlcqDBaH0TikiciIgP1PEXRWjhj4tv+p+FaIsUY/yN6jhuvz+RB9cr8ZJ - EdsKtqH0QSlii2K7i1qnPmNhM78vf2f1Ttg+t+Fg7cFH+avV7L75/Yk+KAVtUqCnce72OWTfy8bxxuMw - R5ilqC/Z2dgn8lnwqvxVKPmkBNW/r0ZOZw4GRQ7qzl+ndj2e35/og1LQFoWw1DCkXEvB6ZunkdqZirTr - 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 @@ -732,52 +658,45 @@ - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAABSgAAAUoAQg00hkAAAAGYktHRAAAAAAAAPlDu38AAAAJdnBBZwAAADAAAAAwAM7u - jFcAAAAldEVYdGNyZWF0ZS1kYXRlADIwMDktMTEtMTVUMTc6MDI6MzUtMDc6MDAQkIWmAAAAJXRFWHRk - YXRlOmNyZWF0ZQAyMDEwLTAyLTIwVDIzOjI2OjE1LTA3OjAwBjtcgQAAACV0RVh0ZGF0ZTptb2RpZnkA - MjAxMC0wMS0xMVQwOTozMToxNS0wNzowMNGhzrMAAABndEVYdExpY2Vuc2UAaHR0cDovL2NyZWF0aXZl - Y29tbW9ucy5vcmcvbGljZW5zZXMvYnktc2EvMy4wLyBvciBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9y - Zy9saWNlbnNlcy9MR1BMLzIuMS9bjzxjAAAAJXRFWHRtb2RpZnktZGF0ZQAyMDA5LTAzLTE5VDEwOjUz - OjA1LTA2OjAwLAW8TwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAATdEVYdFNv - dXJjZQBPeHlnZW4gSWNvbnPsGK7oAAAAJ3RFWHRTb3VyY2VfVVJMAGh0dHA6Ly93d3cub3h5Z2VuLWlj - b25zLm9yZy/vN6rLAAAIc0lEQVRoQ9WXfVCU1xXGz7vvAsuygBBN1bR+JVIFF5CgCIoSiUZURBFFRBQV - RCkKAoIo38i3EV1BDSqK7i4g6oqo4BdRUxNrkzaT6TSdTjOddjr5ozOZTtP+lXT69FzMzjiZ23mNBTrs - zO+fZ+679zx7nnPfuwRgVCMVRxNScTQhFUcTUlELSiXDtLRptSHZIa20jrxla0YKqagFraDSuXlzUdFR - AYql31EMhcjWjQRSUQsu+qp3ojcGvhiAMcEoTHxFq2izbO1wIxW14IIdDK78+QqOPzqOqWlThYlvuDNH - qJx0smeGC6mohdNA62etsPzagpanLYjYHyFMgFZSH8XRGNlzw4FU1MJp4Ngvj6HhSQManzbC8rEFiUcT - n5kYwbmQilo4DTR82IDKR5Wo+qAK1Y+rUfdRHbKsWTDEG57NRSxtkT0/lEhFLTgiDgaHHhxC0f0iHBw4 - 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 diff --git a/EasyModbus_Net60/AdvancedModbusClient.cs b/EasyModbus_Net60/AdvancedModbusClient.cs new file mode 100644 index 0000000..2c322f0 --- /dev/null +++ b/EasyModbus_Net60/AdvancedModbusClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EasyModbus +{ + public partial class ModbusClient + { + /* + public enum DataType { Short = 0, UShort = 1, Long = 2, ULong = 3, Float = 4, Double = 5 }; + public object[] ReadHoldingRegisters(int startingAddress, int quantity, DataType dataType, RegisterOrder registerOrder) + { + int quantityToRead = quantity; + if (dataType == DataType.Long | dataType == DataType.ULong | dataType == DataType.Float) + quantityToRead = quantity * 2; + if (dataType == DataType.Float) + quantityToRead = quantity * 4; + int[] response = this.ReadHoldingRegisters(startingAddress, quantityToRead); + switch (dataType) + { + case DataType.Short: return response.Cast().ToArray(); + break; + default: return response.Cast().ToArray(); + break; + + } + + + } + */ + } +} diff --git a/EasyModbus_Net60/EasyModbus_Net60.csproj b/EasyModbus_Net60/EasyModbus_Net60.csproj new file mode 100644 index 0000000..27f1e16 --- /dev/null +++ b/EasyModbus_Net60/EasyModbus_Net60.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + EasyModbus + EasyModbus + + + + + + + diff --git a/EasyModbus_Net60/Exceptions/Exceptions.cs b/EasyModbus_Net60/Exceptions/Exceptions.cs new file mode 100644 index 0000000..8d1db4f --- /dev/null +++ b/EasyModbus_Net60/Exceptions/Exceptions.cs @@ -0,0 +1,211 @@ +/* +Copyright (c) 2018-2020 Rossmann-Engineering +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software +and associated documentation files (the "Software"), +to deal in the Software without restriction, +including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission +notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Runtime.Serialization; + +namespace EasyModbus.Exceptions +{ + /// + /// Exception to be thrown if serial port is not opened + /// + public class SerialPortNotOpenedException : ModbusException + { + public SerialPortNotOpenedException() + : base() + { + } + + public SerialPortNotOpenedException(string message) + : base(message) + { + } + + public SerialPortNotOpenedException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected SerialPortNotOpenedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if Connection to Modbus device failed + /// + public class ConnectionException : ModbusException + { + public ConnectionException() + : base() + { + } + + public ConnectionException(string message) + : base(message) + { + } + + public ConnectionException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected ConnectionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if Modbus Server returns error code "Function code not supported" + /// + public class FunctionCodeNotSupportedException : ModbusException + { + public FunctionCodeNotSupportedException() + : base() + { + } + + public FunctionCodeNotSupportedException(string message) + : base(message) + { + } + + public FunctionCodeNotSupportedException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected FunctionCodeNotSupportedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if Modbus Server returns error code "quantity invalid" + /// + public class QuantityInvalidException : ModbusException + { + public QuantityInvalidException() + : base() + { + } + + public QuantityInvalidException(string message) + : base(message) + { + } + + public QuantityInvalidException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected QuantityInvalidException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if Modbus Server returns error code "starting adddress and quantity invalid" + /// + public class StartingAddressInvalidException : ModbusException + { + public StartingAddressInvalidException() + : base() + { + } + + public StartingAddressInvalidException(string message) + : base(message) + { + } + + public StartingAddressInvalidException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected StartingAddressInvalidException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if Modbus Server returns error code "Function Code not executed (0x04)" + /// + public class ModbusException : Exception + { + public ModbusException() + : base() + { + } + + public ModbusException(string message) + : base(message) + { + } + + public ModbusException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected ModbusException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// Exception to be thrown if CRC Check failed + /// + public class CRCCheckFailedException : ModbusException + { + public CRCCheckFailedException() + : base() + { + } + + public CRCCheckFailedException(string message) + : base(message) + { + } + + public CRCCheckFailedException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected CRCCheckFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + +} diff --git a/EasyModbus_Net60/ModbusClient.cs b/EasyModbus_Net60/ModbusClient.cs new file mode 100644 index 0000000..c1e8f38 --- /dev/null +++ b/EasyModbus_Net60/ModbusClient.cs @@ -0,0 +1,2851 @@ +/* +Copyright (c) 2018-2020 Rossmann-Engineering +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software +and associated documentation files (the "Software"), +to deal in the Software without restriction, +including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission +notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +using System; +using System.Net.Sockets; +using System.Net; +using System.IO.Ports; +using System.Reflection; +using System.Text; +using System.Collections.Generic; + +namespace EasyModbus +{ + /// + /// Implements a ModbusClient. + /// + public partial class ModbusClient + { + public enum RegisterOrder { LowHigh = 0, HighLow = 1 }; + private bool debug=false; + private TcpClient tcpClient; + private string ipAddress = "127.0.0.1"; + private int port = 502; + private uint transactionIdentifierInternal = 0; + private byte [] transactionIdentifier = new byte[2]; + private byte [] protocolIdentifier = new byte[2]; + private byte[] crc = new byte[2]; + private byte [] length = new byte[2]; + private byte unitIdentifier = 0x01; + private byte functionCode; + private byte [] startingAddress = new byte[2]; + private byte [] quantity = new byte[2]; + private bool udpFlag = false; + private int portOut; + private int baudRate = 9600; + private int connectTimeout = 1000; + public byte[] receiveData; + public byte[] sendData; + private SerialPort serialport; + private Parity parity = Parity.Even; + private StopBits stopBits = StopBits.One; + private bool connected = false; + public int NumberOfRetries { get; set; } = 3; + private int countRetries = 0; + + public delegate void ReceiveDataChangedHandler(object sender); + public event ReceiveDataChangedHandler ReceiveDataChanged; + + public delegate void SendDataChangedHandler(object sender); + public event SendDataChangedHandler SendDataChanged; + + public delegate void ConnectedChangedHandler(object sender); + public event ConnectedChangedHandler ConnectedChanged; + + NetworkStream stream; + + /// + /// Constructor which determines the Master ip-address and the Master Port. + /// + /// IP-Address of the Master device + /// Listening port of the Master device (should be 502) + public ModbusClient(string ipAddress, int port) + { + if (debug) StoreLogData.Instance.Store("EasyModbus library initialized for Modbus-TCP, IPAddress: " + ipAddress + ", Port: "+port ,System.DateTime.Now); +#if (!COMMERCIAL) + Console.WriteLine("EasyModbus Client Library Version: " + Assembly.GetExecutingAssembly().GetName().Version.ToString()); + Console.WriteLine("Copyright (c) Stefan Rossmann Engineering Solutions"); + Console.WriteLine(); +#endif + this.ipAddress = ipAddress; + this.port = port; + } + + /// + /// Constructor which determines the Serial-Port + /// + /// Serial-Port Name e.G. "COM1" + public ModbusClient(string serialPort) + { + if (debug) StoreLogData.Instance.Store("EasyModbus library initialized for Modbus-RTU, COM-Port: " + serialPort ,System.DateTime.Now); +#if (!COMMERCIAL) + Console.WriteLine("EasyModbus Client Library Version: " + Assembly.GetExecutingAssembly().GetName().Version.ToString()); + Console.WriteLine("Copyright (c) Stefan Rossmann Engineering Solutions"); + Console.WriteLine(); +#endif + this.serialport = new SerialPort(); + serialport.PortName = serialPort; + serialport.BaudRate = baudRate; + serialport.Parity = parity; + serialport.StopBits = stopBits; + serialport.WriteTimeout = 10000; + serialport.ReadTimeout = connectTimeout; + + serialport.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); + } + + /// + /// Parameterless constructor + /// + public ModbusClient() + { + if (debug) StoreLogData.Instance.Store("EasyModbus library initialized for Modbus-TCP" ,System.DateTime.Now); +#if (!COMMERCIAL) + Console.WriteLine("EasyModbus Client Library Version: " + Assembly.GetExecutingAssembly().GetName().Version.ToString()); + Console.WriteLine("Copyright (c) Stefan Rossmann Engineering Solutions"); + Console.WriteLine(); +#endif + } + + /// + /// Establish connection to Master device in case of Modbus TCP. Opens COM-Port in case of Modbus RTU + /// + public void Connect() + { + if (serialport != null) + { + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("Open Serial port " + serialport.PortName,System.DateTime.Now); + serialport.BaudRate = baudRate; + serialport.Parity = parity; + serialport.StopBits = stopBits; + serialport.WriteTimeout = 10000; + serialport.ReadTimeout = connectTimeout; + serialport.Open(); + connected = true; + + + } + if (ConnectedChanged != null) + try + { + ConnectedChanged(this); + } + catch + { + + } + return; + } + if (!udpFlag) + { + if (debug) StoreLogData.Instance.Store("Open TCP-Socket, IP-Address: " + ipAddress + ", Port: " + port, System.DateTime.Now); + tcpClient = new TcpClient(); + var result = tcpClient.BeginConnect(ipAddress, port, null, null); + var success = result.AsyncWaitHandle.WaitOne(connectTimeout); + if (!success) + { + throw new EasyModbus.Exceptions.ConnectionException("connection timed out"); + } + tcpClient.EndConnect(result); + + //tcpClient = new TcpClient(ipAddress, port); + stream = tcpClient.GetStream(); + stream.ReadTimeout = connectTimeout; + connected = true; + } + else + { + tcpClient = new TcpClient(); + connected = true; + } + if (ConnectedChanged != null) + try + { + ConnectedChanged(this); + } + catch + { + + } + } + + /// + /// Establish connection to Master device in case of Modbus TCP. + /// + public void Connect(string ipAddress, int port) + { + if (!udpFlag) + { + if (debug) StoreLogData.Instance.Store("Open TCP-Socket, IP-Address: " + ipAddress + ", Port: " + port, System.DateTime.Now); + tcpClient = new TcpClient(); + var result = tcpClient.BeginConnect(ipAddress, port, null, null); + var success = result.AsyncWaitHandle.WaitOne(connectTimeout); + if (!success) + { + throw new EasyModbus.Exceptions.ConnectionException("connection timed out"); + } + tcpClient.EndConnect(result); + + //tcpClient = new TcpClient(ipAddress, port); + stream = tcpClient.GetStream(); + stream.ReadTimeout = connectTimeout; + connected = true; + } + else + { + tcpClient = new TcpClient(); + connected = true; + } + + if (ConnectedChanged != null) + ConnectedChanged(this); + } + + /// + /// Converts two ModbusRegisters to Float - Example: EasyModbus.ModbusClient.ConvertRegistersToFloat(modbusClient.ReadHoldingRegisters(19,2)) + /// + /// Two Register values received from Modbus + /// Connected float value + public static float ConvertRegistersToFloat(int[] registers) + { + if (registers.Length != 2) + throw new ArgumentException("Input Array length invalid - Array langth must be '2'"); + int highRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = BitConverter.GetBytes(highRegister); + byte[] lowRegisterBytes = BitConverter.GetBytes(lowRegister); + byte[] floatBytes = { + lowRegisterBytes[0], + lowRegisterBytes[1], + highRegisterBytes[0], + highRegisterBytes[1] + }; + return BitConverter.ToSingle(floatBytes, 0); + } + + /// + /// Converts two ModbusRegisters to Float, Registers can by swapped + /// + /// Two Register values received from Modbus + /// Desired Word Order (Low Register first or High Register first + /// Connected float value + public static float ConvertRegistersToFloat(int[] registers, RegisterOrder registerOrder) + { + int [] swappedRegisters = {registers[0],registers[1]}; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[] {registers[1],registers[0]}; + return ConvertRegistersToFloat(swappedRegisters); + } + + /// + /// Converts two ModbusRegisters to 32 Bit Integer value + /// + /// Two Register values received from Modbus + /// Connected 32 Bit Integer value + public static Int32 ConvertRegistersToInt(int[] registers) + { + if (registers.Length != 2) + throw new ArgumentException("Input Array length invalid - Array langth must be '2'"); + int highRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = BitConverter.GetBytes(highRegister); + byte[] lowRegisterBytes = BitConverter.GetBytes(lowRegister); + byte[] doubleBytes = { + lowRegisterBytes[0], + lowRegisterBytes[1], + highRegisterBytes[0], + highRegisterBytes[1] + }; + return BitConverter.ToInt32(doubleBytes, 0); + } + + /// + /// Converts two ModbusRegisters to 32 Bit Integer Value - Registers can be swapped + /// + /// Two Register values received from Modbus + /// Desired Word Order (Low Register first or High Register first + /// Connecteds 32 Bit Integer value + public static Int32 ConvertRegistersToInt(int[] registers, RegisterOrder registerOrder) + { + int[] swappedRegisters = { registers[0], registers[1] }; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[] { registers[1], registers[0] }; + return ConvertRegistersToInt(swappedRegisters); + } + + /// + /// Convert four 16 Bit Registers to 64 Bit Integer value Register Order "LowHigh": Reg0: Low Word.....Reg3: High Word, "HighLow": Reg0: High Word.....Reg3: Low Word + /// + /// four Register values received from Modbus + /// 64 bit value + public static Int64 ConvertRegistersToLong(int[] registers) + { + if (registers.Length != 4) + throw new ArgumentException("Input Array length invalid - Array langth must be '4'"); + int highRegister = registers[3]; + int highLowRegister = registers[2]; + int lowHighRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = BitConverter.GetBytes(highRegister); + byte[] highLowRegisterBytes = BitConverter.GetBytes(highLowRegister); + byte[] lowHighRegisterBytes = BitConverter.GetBytes(lowHighRegister); + byte[] lowRegisterBytes = BitConverter.GetBytes(lowRegister); + byte[] longBytes = { + lowRegisterBytes[0], + lowRegisterBytes[1], + lowHighRegisterBytes[0], + lowHighRegisterBytes[1], + highLowRegisterBytes[0], + highLowRegisterBytes[1], + highRegisterBytes[0], + highRegisterBytes[1] + }; + return BitConverter.ToInt64(longBytes, 0); + } + + /// + /// Convert four 16 Bit Registers to 64 Bit Integer value - Registers can be swapped + /// + /// four Register values received from Modbus + /// Desired Word Order (Low Register first or High Register first + /// Connected 64 Bit Integer value + public static Int64 ConvertRegistersToLong(int[] registers, RegisterOrder registerOrder) + { + if (registers.Length != 4) + throw new ArgumentException("Input Array length invalid - Array langth must be '4'"); + int[] swappedRegisters = { registers[0], registers[1], registers[2], registers[3] }; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[] { registers[3], registers[2], registers[1], registers[0] }; + return ConvertRegistersToLong(swappedRegisters); + } + + /// + /// Convert four 16 Bit Registers to 64 Bit double prec. value Register Order "LowHigh": Reg0: Low Word.....Reg3: High Word, "HighLow": Reg0: High Word.....Reg3: Low Word + /// + /// four Register values received from Modbus + /// 64 bit value + public static double ConvertRegistersToDouble(int[] registers) + { + if (registers.Length != 4) + throw new ArgumentException("Input Array length invalid - Array langth must be '4'"); + int highRegister = registers[3]; + int highLowRegister = registers[2]; + int lowHighRegister = registers[1]; + int lowRegister = registers[0]; + byte[] highRegisterBytes = BitConverter.GetBytes(highRegister); + byte[] highLowRegisterBytes = BitConverter.GetBytes(highLowRegister); + byte[] lowHighRegisterBytes = BitConverter.GetBytes(lowHighRegister); + byte[] lowRegisterBytes = BitConverter.GetBytes(lowRegister); + byte[] longBytes = { + lowRegisterBytes[0], + lowRegisterBytes[1], + lowHighRegisterBytes[0], + lowHighRegisterBytes[1], + highLowRegisterBytes[0], + highLowRegisterBytes[1], + highRegisterBytes[0], + highRegisterBytes[1] + }; + return BitConverter.ToDouble(longBytes, 0); + } + + /// + /// Convert four 16 Bit Registers to 64 Bit double prec. value - Registers can be swapped + /// + /// four Register values received from Modbus + /// Desired Word Order (Low Register first or High Register first + /// Connected double prec. float value + public static double ConvertRegistersToDouble(int[] registers, RegisterOrder registerOrder) + { + if (registers.Length != 4) + throw new ArgumentException("Input Array length invalid - Array langth must be '4'"); + int[] swappedRegisters = { registers[0], registers[1], registers[2], registers[3] }; + if (registerOrder == RegisterOrder.HighLow) + swappedRegisters = new int[] { registers[3], registers[2], registers[1], registers[0] }; + return ConvertRegistersToDouble(swappedRegisters); + } + + /// + /// Converts float to two ModbusRegisters - Example: modbusClient.WriteMultipleRegisters(24, EasyModbus.ModbusClient.ConvertFloatToTwoRegisters((float)1.22)); + /// + /// Float value which has to be converted into two registers + /// Register values + public static int[] ConvertFloatToRegisters(float floatValue) + { + byte[] floatBytes = BitConverter.GetBytes(floatValue); + byte[] highRegisterBytes = + { + floatBytes[2], + floatBytes[3], + 0, + 0 + }; + byte[] lowRegisterBytes = + { + + floatBytes[0], + floatBytes[1], + 0, + 0 + }; + int[] returnValue = + { + BitConverter.ToInt32(lowRegisterBytes,0), + BitConverter.ToInt32(highRegisterBytes,0) + }; + return returnValue; + } + + /// + /// Converts float to two ModbusRegisters Registers - Registers can be swapped + /// + /// Float value which has to be converted into two registers + /// Desired Word Order (Low Register first or High Register first + /// Register values + public static int[] ConvertFloatToRegisters(float floatValue, RegisterOrder registerOrder) + { + int[] registerValues = ConvertFloatToRegisters(floatValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new Int32[] { registerValues[1], registerValues[0] }; + return returnValue; + } + + /// + /// Converts 32 Bit Value to two ModbusRegisters + /// + /// Int value which has to be converted into two registers + /// Register values + public static int[] ConvertIntToRegisters(Int32 intValue) + { + byte[] doubleBytes = BitConverter.GetBytes(intValue); + byte[] highRegisterBytes = + { + doubleBytes[2], + doubleBytes[3], + 0, + 0 + }; + byte[] lowRegisterBytes = + { + + doubleBytes[0], + doubleBytes[1], + 0, + 0 + }; + int[] returnValue = + { + BitConverter.ToInt32(lowRegisterBytes,0), + BitConverter.ToInt32(highRegisterBytes,0) + }; + return returnValue; + } + + /// + /// Converts 32 Bit Value to two ModbusRegisters Registers - Registers can be swapped + /// + /// Double value which has to be converted into two registers + /// Desired Word Order (Low Register first or High Register first + /// Register values + public static int[] ConvertIntToRegisters(Int32 intValue, RegisterOrder registerOrder) + { + int[] registerValues = ConvertIntToRegisters(intValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new Int32[] { registerValues[1], registerValues[0] }; + return returnValue; + } + + /// + /// Converts 64 Bit Value to four ModbusRegisters + /// + /// long value which has to be converted into four registers + /// Register values + public static int[] ConvertLongToRegisters(Int64 longValue) + { + byte[] longBytes = BitConverter.GetBytes(longValue); + byte[] highRegisterBytes = + { + longBytes[6], + longBytes[7], + 0, + 0 + }; + byte[] highLowRegisterBytes = + { + longBytes[4], + longBytes[5], + 0, + 0 + }; + byte[] lowHighRegisterBytes = + { + longBytes[2], + longBytes[3], + 0, + 0 + }; + byte[] lowRegisterBytes = + { + + longBytes[0], + longBytes[1], + 0, + 0 + }; + int[] returnValue = + { + BitConverter.ToInt32(lowRegisterBytes,0), + BitConverter.ToInt32(lowHighRegisterBytes,0), + BitConverter.ToInt32(highLowRegisterBytes,0), + BitConverter.ToInt32(highRegisterBytes,0) + }; + return returnValue; + } + + /// + /// Converts 64 Bit Value to four ModbusRegisters - Registers can be swapped + /// + /// long value which has to be converted into four registers + /// Desired Word Order (Low Register first or High Register first + /// Register values + public static int[] ConvertLongToRegisters(Int64 longValue, RegisterOrder registerOrder) + { + int[] registerValues = ConvertLongToRegisters(longValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new int[] { registerValues[3], registerValues[2], registerValues[1], registerValues[0] }; + return returnValue; + } + + /// + /// Converts 64 Bit double prec Value to four ModbusRegisters + /// + /// double value which has to be converted into four registers + /// Register values + public static int[] ConvertDoubleToRegisters(double doubleValue) + { + byte[] doubleBytes = BitConverter.GetBytes(doubleValue); + byte[] highRegisterBytes = + { + doubleBytes[6], + doubleBytes[7], + 0, + 0 + }; + byte[] highLowRegisterBytes = + { + doubleBytes[4], + doubleBytes[5], + 0, + 0 + }; + byte[] lowHighRegisterBytes = + { + doubleBytes[2], + doubleBytes[3], + 0, + 0 + }; + byte[] lowRegisterBytes = + { + + doubleBytes[0], + doubleBytes[1], + 0, + 0 + }; + int[] returnValue = + { + BitConverter.ToInt32(lowRegisterBytes,0), + BitConverter.ToInt32(lowHighRegisterBytes,0), + BitConverter.ToInt32(highLowRegisterBytes,0), + BitConverter.ToInt32(highRegisterBytes,0) + }; + return returnValue; + } + + /// + /// Converts 64 Bit double prec. Value to four ModbusRegisters - Registers can be swapped + /// + /// double value which has to be converted into four registers + /// Desired Word Order (Low Register first or High Register first + /// Register values + public static int[] ConvertDoubleToRegisters(double doubleValue, RegisterOrder registerOrder) + { + int[] registerValues = ConvertDoubleToRegisters(doubleValue); + int[] returnValue = registerValues; + if (registerOrder == RegisterOrder.HighLow) + returnValue = new int[] { registerValues[3], registerValues[2], registerValues[1], registerValues[0] }; + return returnValue; + } + + /// + /// Converts 16 - Bit Register values to String + /// + /// Register array received via Modbus + /// First Register containing the String to convert + /// number of characters in String (must be even) + /// Converted String + public static string ConvertRegistersToString(int[] registers, int offset, int stringLength) + { + byte[] result = new byte[stringLength]; + byte[] registerResult = new byte[2]; + + for (int i = 0; i < stringLength/2; i++) + { + registerResult = BitConverter.GetBytes(registers[offset + i]); + result[i * 2] = registerResult[0]; + result[i * 2 + 1] = registerResult[1]; + } + return System.Text.Encoding.Default.GetString(result); + } + + /// + /// Converts a String to 16 - Bit Registers + /// + /// Register array received via Modbus + /// Converted String + public static int[] ConvertStringToRegisters(string stringToConvert) + { + byte[] array = System.Text.Encoding.ASCII.GetBytes(stringToConvert); + int[] returnarray = new int[stringToConvert.Length / 2 + stringToConvert.Length % 2]; + for (int i = 0; i < returnarray.Length; i++) + { + returnarray[i] = array[i * 2]; + if (i*2 +1< array.Length) + { + returnarray[i] = returnarray[i] | ((int)array[i * 2 + 1] << 8); + } + } + return returnarray; + } + + + /// + /// Calculates the CRC16 for Modbus-RTU + /// + /// Byte buffer to send + /// Number of bytes to calculate CRC + /// First byte in buffer to start calculating CRC + public static UInt16 calculateCRC(byte[] data, UInt16 numberOfBytes, int startByte) + { + byte[] auchCRCHi = { + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40 + }; + + byte[] auchCRCLo = { + 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, + 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, + 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, + 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, + 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, + 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, + 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, + 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, + 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, + 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, + 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, + 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, + 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, + 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, + 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, + 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, + 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, + 0x40 + }; + UInt16 usDataLen = numberOfBytes; + byte uchCRCHi = 0xFF ; + byte uchCRCLo = 0xFF ; + int i = 0; + int uIndex ; + while (usDataLen>0) + { + usDataLen--; + if ((i + startByte) < data.Length) + { + uIndex = uchCRCLo ^ data[i + startByte]; + uchCRCLo = (byte)(uchCRCHi ^ auchCRCHi[uIndex]); + uchCRCHi = auchCRCLo[uIndex]; + } + i++; + } + return (UInt16)((UInt16)uchCRCHi << 8 | uchCRCLo); + } + + private bool dataReceived = false; + private bool receiveActive = false; + private byte[] readBuffer = new byte[256]; + private int bytesToRead = 0; + private int akjjjctualPositionToRead = 0; + DateTime dateTimeLastRead; +/* + private void DataReceivedHandler(object sender, + SerialDataReceivedEventArgs e) + { + long ticksWait = TimeSpan.TicksPerMillisecond * 2000; + SerialPort sp = (SerialPort)sender; + + if (bytesToRead == 0 || sp.BytesToRead == 0) + { + actualPositionToRead = 0; + sp.DiscardInBuffer(); + dataReceived = false; + receiveActive = false; + return; + } + + if (actualPositionToRead == 0 && !dataReceived) + readBuffer = new byte[256]; + + //if ((DateTime.Now.Ticks - dateTimeLastRead.Ticks) > ticksWait) + //{ + // readBuffer = new byte[256]; + // actualPositionToRead = 0; + //} + int numberOfBytesInBuffer = sp.BytesToRead; + sp.Read(readBuffer, actualPositionToRead, ((numberOfBytesInBuffer + actualPositionToRead) > readBuffer.Length) ? 0 : numberOfBytesInBuffer); + actualPositionToRead = actualPositionToRead + numberOfBytesInBuffer; + //sp.DiscardInBuffer(); + //if (DetectValidModbusFrame(readBuffer, (actualPositionToRead < readBuffer.Length) ? actualPositionToRead : readBuffer.Length) | bytesToRead <= actualPositionToRead) + if (actualPositionToRead >= bytesToRead) + { + + dataReceived = true; + bytesToRead = 0; + actualPositionToRead = 0; + if (debug) StoreLogData.Instance.Store("Received Serial-Data: " + BitConverter.ToString(readBuffer), System.DateTime.Now); + + } + + + //dateTimeLastRead = DateTime.Now; + } + */ + + + private void DataReceivedHandler(object sender, + SerialDataReceivedEventArgs e) + { + serialport.DataReceived -= DataReceivedHandler; + + //while (receiveActive | dataReceived) + // System.Threading.Thread.Sleep(10); + receiveActive = true; + + const long ticksWait = TimeSpan.TicksPerMillisecond * 2000;//((40*10000000) / this.baudRate); + + + SerialPort sp = (SerialPort)sender; + if (bytesToRead == 0) + { + sp.DiscardInBuffer(); + receiveActive = false; + serialport.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); + return; + } + readBuffer = new byte[256]; + int numbytes=0; + int actualPositionToRead = 0; + DateTime dateTimeLastRead = DateTime.Now; + do{ + try { + dateTimeLastRead = DateTime.Now; + while ((sp.BytesToRead) == 0) + { + System.Threading.Thread.Sleep(10); + if ((DateTime.Now.Ticks - dateTimeLastRead.Ticks) > ticksWait) + break; + } + numbytes=sp.BytesToRead; + + + byte[] rxbytearray = new byte[numbytes]; + sp.Read(rxbytearray, 0, numbytes); + Array.Copy(rxbytearray,0, readBuffer,actualPositionToRead, (actualPositionToRead + rxbytearray.Length) <= bytesToRead ? rxbytearray.Length : bytesToRead - actualPositionToRead); + + actualPositionToRead = actualPositionToRead + rxbytearray.Length; + + } + catch (Exception){ + + } + + if (bytesToRead <= actualPositionToRead) + break; + + if (DetectValidModbusFrame(readBuffer, (actualPositionToRead < readBuffer.Length) ? actualPositionToRead : readBuffer.Length) | bytesToRead <= actualPositionToRead) + break; + } + while ((DateTime.Now.Ticks - dateTimeLastRead.Ticks) < ticksWait) ; + + //10.000 Ticks in 1 ms + + receiveData = new byte[actualPositionToRead]; + Array.Copy(readBuffer, 0, receiveData, 0, (actualPositionToRead < readBuffer.Length) ? actualPositionToRead: readBuffer.Length); + if (debug) StoreLogData.Instance.Store("Received Serial-Data: "+BitConverter.ToString(readBuffer) ,System.DateTime.Now); + bytesToRead = 0; + + + + + dataReceived = true; + receiveActive = false; + serialport.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); + if (ReceiveDataChanged != null) + { + + ReceiveDataChanged(this); + + } + + //sp.DiscardInBuffer(); + } + + public static bool DetectValidModbusFrame(byte[] readBuffer, int length) + { + // minimum length 6 bytes + if (length < 6) + return false; + //SlaveID correct + if ((readBuffer[0] < 1) | (readBuffer[0] > 247)) + return false; + //CRC correct? + byte[] crc = new byte[2]; + crc = BitConverter.GetBytes(calculateCRC(readBuffer, (ushort)(length-2), 0)); + if (crc[0] != readBuffer[length-2] | crc[1] != readBuffer[length-1]) + return false; + return true; + } + + + + /// + /// Read Discrete Inputs from Server device (FC2). + /// + /// First discrete input to read + /// Number of discrete Inputs to read + /// Boolean Array which contains the discrete Inputs + public bool[] ReadDiscreteInputs(int startingAddress, int quantity) + { + if (debug) StoreLogData.Instance.Store("FC2 (Read Discrete Inputs from Master device), StartingAddress: "+ startingAddress+", Quantity: " +quantity, System.DateTime.Now); + transactionIdentifierInternal ++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport==null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + if (startingAddress > 65535 | quantity >2000) + { + if (debug) StoreLogData.Instance.Store("ArgumentException Throwed", System.DateTime.Now); + throw new ArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 2000"); + } + bool[] response; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int) 0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x02; + this.startingAddress = BitConverter.GetBytes(startingAddress); + this.quantity = BitConverter.GetBytes(quantity); + Byte[] data = new byte[] + { + this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + this.quantity[1], + this.quantity[0], + this.crc[0], + this.crc[1] + }; + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + + if (serialport != null) + { + dataReceived = false; + if (quantity % 8 == 0) + bytesToRead = 5 + quantity / 8; + else + bytesToRead = 6 + quantity / 8; + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + + + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x82 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x82 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x82 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x82 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data[8]+3), 6)); + if ((crc[0] != data[data[8] + 9] | crc[1] != data[data[8] + 10]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + return ReadDiscreteInputs(startingAddress, quantity); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + } + else + { + countRetries++; + return ReadDiscreteInputs(startingAddress, quantity); + } + } + } + response = new bool[quantity]; + for (int i = 0; i < quantity; i++) + { + int intData = data[9+i/8]; + int mask = Convert.ToInt32(Math.Pow(2, (i%8))); + response[i] = Convert.ToBoolean((intData & mask)/mask); + } + return (response); + } + + + /// + /// Read Coils from Server device (FC1). + /// + /// First coil to read + /// Numer of coils to read + /// Boolean Array which contains the coils + public bool[] ReadCoils(int startingAddress, int quantity) + { + if (debug) StoreLogData.Instance.Store("FC1 (Read Coils from Master device), StartingAddress: "+ startingAddress+", Quantity: " +quantity, System.DateTime.Now); + transactionIdentifierInternal++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + if (startingAddress > 65535 | quantity >2000) + { + if (debug) StoreLogData.Instance.Store("ArgumentException Throwed", System.DateTime.Now); + throw new ArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 2000"); + } + bool[] response; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int) 0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x01; + this.startingAddress = BitConverter.GetBytes(startingAddress); + this.quantity = BitConverter.GetBytes(quantity); + Byte[] data = new byte[]{ + this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + this.quantity[1], + this.quantity[0], + this.crc[0], + this.crc[1] + }; + + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + if (serialport != null) + { + dataReceived = false; + if (quantity % 8 == 0) + bytesToRead = 5 + quantity/8; + else + bytesToRead = 6 + quantity/8; + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send MocbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x81 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x81 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x81 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x81 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data[8]+3), 6)); + if ((crc[0] != data[data[8]+9] | crc[1] != data[data[8]+10]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + return ReadCoils(startingAddress, quantity); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + } + else + { + countRetries++; + return ReadCoils(startingAddress, quantity); + } + } + } + response = new bool[quantity]; + for (int i = 0; i < quantity; i++) + { + int intData = data[9+i/8]; + int mask = Convert.ToInt32(Math.Pow(2, (i%8))); + response[i] = Convert.ToBoolean((intData & mask)/mask); + } + return (response); + } + + + /// + /// Read Holding Registers from Master device (FC3). + /// + /// First holding register to be read + /// Number of holding registers to be read + /// Int Array which contains the holding registers + public int[] ReadHoldingRegisters(int startingAddress, int quantity) + { + if (debug) StoreLogData.Instance.Store("FC3 (Read Holding Registers from Master device), StartingAddress: "+ startingAddress+", Quantity: " +quantity, System.DateTime.Now); + transactionIdentifierInternal++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + if (startingAddress > 65535 | quantity >125) + { + if (debug) StoreLogData.Instance.Store("ArgumentException Throwed", System.DateTime.Now); + throw new ArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 125"); + } + int[] response; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int) 0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x03; + this.startingAddress = BitConverter.GetBytes(startingAddress); + this.quantity = BitConverter.GetBytes(quantity); + Byte[] data = new byte[]{ this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + this.quantity[1], + this.quantity[0], + this.crc[0], + this.crc[1] + }; + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 5 + 2 * quantity; +// serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[256]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x83 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x83 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x83 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x83 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data[8]+3), 6)); + if ((crc[0] != data[data[8]+9] | crc[1] != data[data[8]+10])& dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + return ReadHoldingRegisters(startingAddress, quantity); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + } + else + { + countRetries++; + return ReadHoldingRegisters(startingAddress, quantity); + } + + + } + } + response = new int[quantity]; + for (int i = 0; i < quantity; i++) + { + byte lowByte; + byte highByte; + highByte = data[9+i*2]; + lowByte = data[9+i*2+1]; + + data[9+i*2] = lowByte; + data[9+i*2+1] = highByte; + + response[i] = BitConverter.ToInt16(data,(9+i*2)); + } + return (response); + } + + + + /// + /// Read Input Registers from Master device (FC4). + /// + /// First input register to be read + /// Number of input registers to be read + /// Int Array which contains the input registers + public int[] ReadInputRegisters(int startingAddress, int quantity) + { + + if (debug) StoreLogData.Instance.Store("FC4 (Read Input Registers from Master device), StartingAddress: "+ startingAddress+", Quantity: " +quantity, System.DateTime.Now); + transactionIdentifierInternal++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + if (startingAddress > 65535 | quantity >125) + { + if (debug) StoreLogData.Instance.Store("ArgumentException Throwed", System.DateTime.Now); + throw new ArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 125"); + } + int[] response; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int) 0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x04; + this.startingAddress = BitConverter.GetBytes(startingAddress); + this.quantity = BitConverter.GetBytes(quantity); + Byte[] data = new byte[]{ this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + this.quantity[1], + this.quantity[0], + this.crc[0], + this.crc[1] + }; + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 5 + 2 * quantity; + + + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + + } + } + if (data[7] == 0x84 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x84 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x84 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x84 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data[8]+3), 6)); + if ((crc[0] != data[data[8]+9] | crc[1] != data[data[8]+10]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + return ReadInputRegisters(startingAddress, quantity); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + + } + else + { + countRetries++; + return ReadInputRegisters(startingAddress, quantity); + } + + } + } + response = new int[quantity]; + for (int i = 0; i < quantity; i++) + { + byte lowByte; + byte highByte; + highByte = data[9+i*2]; + lowByte = data[9+i*2+1]; + + data[9+i*2] = lowByte; + data[9+i*2+1] = highByte; + + response[i] = BitConverter.ToInt16(data,(9+i*2)); + } + return (response); + } + + + /// + /// Write single Coil to Master device (FC5). + /// + /// Coil to be written + /// Coil Value to be written + public void WriteSingleCoil(int startingAddress, bool value) + { + + if (debug) StoreLogData.Instance.Store("FC5 (Write single coil to Master device), StartingAddress: "+ startingAddress+", Value: " + value, System.DateTime.Now); + transactionIdentifierInternal++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + byte[] coilValue = new byte[2]; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int)0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x05; + this.startingAddress = BitConverter.GetBytes(startingAddress); + if (value == true) + { + coilValue = BitConverter.GetBytes((int)0xFF00); + } + else + { + coilValue = BitConverter.GetBytes((int)0x0000); + } + Byte[] data = new byte[]{ this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + coilValue[1], + coilValue[0], + this.crc[0], + this.crc[1] + }; + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 8; + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length - 2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length - 2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x85 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x85 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x85 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x85 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + if ((crc[0] != data[12] | crc[1] != data[13]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + WriteSingleCoil(startingAddress, value); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + + } + else + { + countRetries++; + WriteSingleCoil(startingAddress, value); + } + } + } + } + + + /// + /// Write single Register to Master device (FC6). + /// + /// Register to be written + /// Register Value to be written + public void WriteSingleRegister(int startingAddress, int value) + { + if (debug) StoreLogData.Instance.Store("FC6 (Write single register to Master device), StartingAddress: "+ startingAddress+", Value: " + value, System.DateTime.Now); + transactionIdentifierInternal++; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + byte[] registerValue = new byte[2]; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int)0x0000); + this.length = BitConverter.GetBytes((int)0x0006); + this.functionCode = 0x06; + this.startingAddress = BitConverter.GetBytes(startingAddress); + registerValue = BitConverter.GetBytes((int)value); + + Byte[] data = new byte[]{ this.transactionIdentifier[1], + this.transactionIdentifier[0], + this.protocolIdentifier[1], + this.protocolIdentifier[0], + this.length[1], + this.length[0], + this.unitIdentifier, + this.functionCode, + this.startingAddress[1], + this.startingAddress[0], + registerValue[1], + registerValue[0], + this.crc[0], + this.crc[1] + }; + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + data[12] = crc[0]; + data[13] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 8; +// serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, 8); + if (debug) + { + byte [] debugData = new byte[8]; + Array.Copy(data, 6, debugData, 0, 8); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[8]; + Array.Copy(data, 6, sendData, 0, 8); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length - 2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length - 2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x86 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x86 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x86 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x86 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + if ((crc[0] != data[12] | crc[1] != data[13]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + WriteSingleRegister(startingAddress, value); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + + } + else + { + countRetries++; + WriteSingleRegister(startingAddress, value); + } + } + } + } + + /// + /// Write multiple coils to Master device (FC15). + /// + /// First coil to be written + /// Coil Values to be written + public void WriteMultipleCoils(int startingAddress, bool[] values) + { + string debugString = ""; + for (int i = 0; i < values.Length;i++) + debugString = debugString + values[i] + " "; + if (debug) StoreLogData.Instance.Store("FC15 (Write multiple coils to Master device), StartingAddress: "+ startingAddress+", Values: " + debugString, System.DateTime.Now); + transactionIdentifierInternal++; + byte byteCount = (byte)((values.Length % 8 != 0 ? values.Length / 8 + 1: (values.Length / 8))); + byte[] quantityOfOutputs = BitConverter.GetBytes((int)values.Length); + byte singleCoilValue = 0; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int)0x0000); + this.length = BitConverter.GetBytes((int)(7+(byteCount))); + this.functionCode = 0x0F; + this.startingAddress = BitConverter.GetBytes(startingAddress); + + + + Byte[] data = new byte[14 +2 + (values.Length % 8 != 0 ? values.Length/8 : (values.Length / 8)-1)]; + data[0] = this.transactionIdentifier[1]; + data[1] = this.transactionIdentifier[0]; + data[2] = this.protocolIdentifier[1]; + data[3] = this.protocolIdentifier[0]; + data[4] = this.length[1]; + data[5] = this.length[0]; + data[6] = this.unitIdentifier; + data[7] = this.functionCode; + data[8] = this.startingAddress[1]; + data[9] = this.startingAddress[0]; + data[10] = quantityOfOutputs[1]; + data[11] = quantityOfOutputs[0]; + data[12] = byteCount; + for (int i = 0; i < values.Length; i++) + { + if ((i % 8) == 0) + singleCoilValue = 0; + byte CoilValue; + if (values[i] == true) + CoilValue = 1; + else + CoilValue = 0; + + + singleCoilValue = (byte)((int)CoilValue<<(i%8) | (int)singleCoilValue); + + data[13 + (i / 8)] = singleCoilValue; + } + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data.Length - 8), 6)); + data[data.Length - 2] = crc[0]; + data[data.Length - 1] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 8; + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte [] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length - 6]; + Array.Copy(data, 6, sendData, 0, data.Length - 6); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x8F & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x8F & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x8F & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x8F & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + if ((crc[0] != data[12] | crc[1] != data[13]) & dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + WriteMultipleCoils(startingAddress, values); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + + } + else + { + countRetries++; + WriteMultipleCoils(startingAddress, values); + } + } + } + } + + /// + /// Write multiple registers to Master device (FC16). + /// + /// First register to be written + /// register Values to be written + public void WriteMultipleRegisters(int startingAddress, int[] values) + { + string debugString = ""; + for (int i = 0; i < values.Length;i++) + debugString = debugString + values[i] + " "; + if (debug) StoreLogData.Instance.Store("FC16 (Write multiple Registers to Server device), StartingAddress: "+ startingAddress+", Values: " + debugString, System.DateTime.Now); + transactionIdentifierInternal++; + byte byteCount = (byte)(values.Length * 2); + byte[] quantityOfOutputs = BitConverter.GetBytes((int)values.Length); + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int)0x0000); + this.length = BitConverter.GetBytes((int)(7+values.Length*2)); + this.functionCode = 0x10; + this.startingAddress = BitConverter.GetBytes(startingAddress); + + Byte[] data = new byte[13+2 + values.Length*2]; + data[0] = this.transactionIdentifier[1]; + data[1] = this.transactionIdentifier[0]; + data[2] = this.protocolIdentifier[1]; + data[3] = this.protocolIdentifier[0]; + data[4] = this.length[1]; + data[5] = this.length[0]; + data[6] = this.unitIdentifier; + data[7] = this.functionCode; + data[8] = this.startingAddress[1]; + data[9] = this.startingAddress[0]; + data[10] = quantityOfOutputs[1]; + data[11] = quantityOfOutputs[0]; + data[12] = byteCount; + for (int i = 0; i < values.Length; i++) + { + byte[] singleRegisterValue = BitConverter.GetBytes((int)values[i]); + data[13 + i*2] = singleRegisterValue[1]; + data[14 + i*2] = singleRegisterValue[0]; + } + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data.Length - 8), 6)); + data[data.Length - 2] = crc[0]; + data[data.Length - 1] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 8; +// serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, data.Length - 6); + + if (debug) + { + byte [] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length - 6]; + Array.Copy(data, 6, sendData, 0, data.Length - 6); + SendDataChanged(this); + + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x90 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x90 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x90 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x90 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + if (serialport != null) + { + crc = BitConverter.GetBytes(calculateCRC(data, 6, 6)); + if ((crc[0] != data[12] | crc[1] != data[13]) &dataReceived) + { + if (debug) StoreLogData.Instance.Store("CRCCheckFailedException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new EasyModbus.Exceptions.CRCCheckFailedException("Response CRC check failed"); + } + else + { + countRetries++; + WriteMultipleRegisters(startingAddress, values); + } + } + else if (!dataReceived) + { + if (debug) StoreLogData.Instance.Store("TimeoutException Throwed", System.DateTime.Now); + if (NumberOfRetries <= countRetries) + { + countRetries = 0; + throw new TimeoutException("No Response from Modbus Slave"); + + } + else + { + countRetries++; + WriteMultipleRegisters(startingAddress, values); + } + } + } + } + + /// + /// Read/Write Multiple Registers (FC23). + /// + /// First input register to read + /// Number of input registers to read + /// First input register to write + /// Values to write + /// Int Array which contains the Holding registers + public int[] ReadWriteMultipleRegisters(int startingAddressRead, int quantityRead, int startingAddressWrite, int[] values) + { + + string debugString = ""; + for (int i = 0; i < values.Length;i++) + debugString = debugString + values[i] + " "; + if (debug) StoreLogData.Instance.Store("FC23 (Read and Write multiple Registers to Server device), StartingAddress Read: "+ startingAddressRead+ ", Quantity Read: "+quantityRead+", startingAddressWrite: " + startingAddressWrite +", Values: " + debugString, System.DateTime.Now); + transactionIdentifierInternal++; + byte [] startingAddressReadLocal = new byte[2]; + byte [] quantityReadLocal = new byte[2]; + byte[] startingAddressWriteLocal = new byte[2]; + byte[] quantityWriteLocal = new byte[2]; + byte writeByteCountLocal = 0; + if (serialport != null) + if (!serialport.IsOpen) + { + if (debug) StoreLogData.Instance.Store("SerialPortNotOpenedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + } + if (tcpClient == null & !udpFlag & serialport == null) + { + if (debug) StoreLogData.Instance.Store("ConnectionException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ConnectionException("connection error"); + } + if (startingAddressRead > 65535 | quantityRead > 125 | startingAddressWrite > 65535 | values.Length > 121) + { + if (debug) StoreLogData.Instance.Store("ArgumentException Throwed", System.DateTime.Now); + throw new ArgumentException("Starting address must be 0 - 65535; quantity must be 0 - 2000"); + } + int[] response; + this.transactionIdentifier = BitConverter.GetBytes((uint)transactionIdentifierInternal); + this.protocolIdentifier = BitConverter.GetBytes((int)0x0000); + this.length = BitConverter.GetBytes((int)11 + values.Length * 2); + this.functionCode = 0x17; + startingAddressReadLocal = BitConverter.GetBytes(startingAddressRead); + quantityReadLocal = BitConverter.GetBytes(quantityRead); + startingAddressWriteLocal = BitConverter.GetBytes(startingAddressWrite); + quantityWriteLocal = BitConverter.GetBytes(values.Length); + writeByteCountLocal = Convert.ToByte(values.Length * 2); + Byte[] data = new byte[17 +2+ values.Length*2]; + data[0] = this.transactionIdentifier[1]; + data[1] = this.transactionIdentifier[0]; + data[2] = this.protocolIdentifier[1]; + data[3] = this.protocolIdentifier[0]; + data[4] = this.length[1]; + data[5] = this.length[0]; + data[6] = this.unitIdentifier; + data[7] = this.functionCode; + data[8] = startingAddressReadLocal[1]; + data[9] = startingAddressReadLocal[0]; + data[10] = quantityReadLocal[1]; + data[11] = quantityReadLocal[0]; + data[12] = startingAddressWriteLocal[1]; + data[13] = startingAddressWriteLocal[0]; + data[14] = quantityWriteLocal[1]; + data[15] = quantityWriteLocal[0]; + data[16] = writeByteCountLocal; + + for (int i = 0; i < values.Length; i++) + { + byte[] singleRegisterValue = BitConverter.GetBytes((int)values[i]); + data[17 + i*2] = singleRegisterValue[1]; + data[18 + i*2] = singleRegisterValue[0]; + } + crc = BitConverter.GetBytes(calculateCRC(data, (ushort)(data.Length - 8), 6)); + data[data.Length - 2] = crc[0]; + data[data.Length - 1] = crc[1]; + if (serialport != null) + { + dataReceived = false; + bytesToRead = 5 + 2*quantityRead; + // serialport.ReceivedBytesThreshold = bytesToRead; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte [] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length - 6]; + Array.Copy(data, 6, sendData, 0, data.Length - 6); + SendDataChanged(this); + } + data = new byte[2100]; + readBuffer = new byte[256]; + DateTime dateTimeSend = DateTime.Now; + byte receivedUnitIdentifier = 0xFF; + while (receivedUnitIdentifier != this.unitIdentifier & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + { + while (dataReceived == false & !((DateTime.Now.Ticks - dateTimeSend.Ticks) > TimeSpan.TicksPerMillisecond * this.connectTimeout)) + System.Threading.Thread.Sleep(1); + data = new byte[2100]; + Array.Copy(readBuffer, 0, data, 6, readBuffer.Length); + receivedUnitIdentifier = data[6]; + } + if (receivedUnitIdentifier != this.unitIdentifier) + data = new byte[2100]; + else + countRetries = 0; + } + else if (tcpClient.Client.Connected | udpFlag) + { + if (udpFlag) + { + UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), port); + udpClient.Send(data, data.Length-2, endPoint); + portOut = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; + udpClient.Client.ReceiveTimeout = 5000; + endPoint = new IPEndPoint(System.Net.IPAddress.Parse(ipAddress), portOut); + data = udpClient.Receive(ref endPoint); + } + else + { + stream.Write(data, 0, data.Length-2); + if (debug) + { + byte [] debugData = new byte[data.Length-2]; + Array.Copy(data, 0, debugData, 0, data.Length-2); + if (debug) StoreLogData.Instance.Store("Send ModbusTCP-Data: "+BitConverter.ToString(debugData) ,System.DateTime.Now); + } + if (SendDataChanged != null) + { + sendData = new byte[data.Length-2]; + Array.Copy(data, 0, sendData, 0, data.Length-2); + SendDataChanged(this); + + } + data = new Byte[2100]; + int NumberOfBytes = stream.Read(data, 0, data.Length); + if (ReceiveDataChanged != null) + { + receiveData = new byte[NumberOfBytes]; + Array.Copy(data, 0, receiveData, 0, NumberOfBytes); + if (debug) StoreLogData.Instance.Store("Receive ModbusTCP-Data: " + BitConverter.ToString(receiveData), System.DateTime.Now); + ReceiveDataChanged(this); + } + } + } + if (data[7] == 0x97 & data[8] == 0x01) + { + if (debug) StoreLogData.Instance.Store("FunctionCodeNotSupportedException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.FunctionCodeNotSupportedException("Function code not supported by master"); + } + if (data[7] == 0x97 & data[8] == 0x02) + { + if (debug) StoreLogData.Instance.Store("StartingAddressInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.StartingAddressInvalidException("Starting address invalid or starting address + quantity invalid"); + } + if (data[7] == 0x97 & data[8] == 0x03) + { + if (debug) StoreLogData.Instance.Store("QuantityInvalidException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.QuantityInvalidException("quantity invalid"); + } + if (data[7] == 0x97 & data[8] == 0x04) + { + if (debug) StoreLogData.Instance.Store("ModbusException Throwed", System.DateTime.Now); + throw new EasyModbus.Exceptions.ModbusException("error reading"); + } + response = new int[quantityRead]; + for (int i = 0; i < quantityRead; i++) + { + byte lowByte; + byte highByte; + highByte = data[9 + i * 2]; + lowByte = data[9 + i * 2 + 1]; + + data[9 + i * 2] = lowByte; + data[9 + i * 2 + 1] = highByte; + + response[i] = BitConverter.ToInt16(data, (9 + i * 2)); + } + return (response); + } + + /// + /// Close connection to Master Device. + /// + public void Disconnect() + { + if (debug) StoreLogData.Instance.Store("Disconnect", System.DateTime.Now); + if (serialport != null) + { + if (serialport.IsOpen & !this.receiveActive) + serialport.Close(); + if (ConnectedChanged != null) + ConnectedChanged(this); + return; + } + if (stream != null) + stream.Close(); + if (tcpClient != null) + tcpClient.Close(); + connected = false; + if (ConnectedChanged != null) + ConnectedChanged(this); + + } + + /// + /// Destructor - Close connection to Master Device. + /// + ~ ModbusClient() + { + if (debug) StoreLogData.Instance.Store("Destructor called - automatically disconnect", System.DateTime.Now); + if (serialport != null) + { + if (serialport.IsOpen) + serialport.Close(); + return; + } + if (tcpClient != null & !udpFlag) + { + if (stream !=null) + stream.Close(); + tcpClient.Close(); + } + } + + /// + /// Returns "TRUE" if Client is connected to Server and "FALSE" if not. In case of Modbus RTU returns if COM-Port is opened + /// + public bool Connected + { + get + { + if (serialport != null) + { + return (serialport.IsOpen); + } + + if (udpFlag & tcpClient != null) + return true; + if (tcpClient == null) + return false; + else + { + return connected; + + } + + } + } + + public bool Available(int timeout) + { + // Ping's the local machine. + System.Net.NetworkInformation.Ping pingSender = new System.Net.NetworkInformation.Ping(); + IPAddress address = System.Net.IPAddress.Parse(ipAddress); + + // Create a buffer of 32 bytes of data to be transmitted. + string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + byte[] buffer = System.Text.Encoding.ASCII.GetBytes(data); + + // Wait 10 seconds for a reply. + System.Net.NetworkInformation.PingReply reply = pingSender.Send(address, timeout, buffer); + + if (reply.Status == System.Net.NetworkInformation.IPStatus.Success) + return true; + else + return false; + } + + /// + /// Gets or Sets the IP-Address of the Server. + /// + public string IPAddress + { + get + { + return ipAddress; + } + set + { + ipAddress = value; + } + } + + /// + /// Gets or Sets the Port were the Modbus-TCP Server is reachable (Standard is 502). + /// + public int Port + { + get + { + return port; + } + set + { + port = value; + } + } + + /// + /// Gets or Sets the UDP-Flag to activate Modbus UDP. + /// + public bool UDPFlag + { + get + { + return udpFlag; + } + set + { + udpFlag = value; + } + } + + /// + /// Gets or Sets the Unit identifier in case of serial connection (Default = 0) + /// + public byte UnitIdentifier + { + get + { + return unitIdentifier; + } + set + { + unitIdentifier = value; + } + } + + + /// + /// Gets or Sets the Baudrate for serial connection (Default = 9600) + /// + public int Baudrate + { + get + { + return baudRate; + } + set + { + baudRate = value; + } + } + + /// + /// Gets or Sets the of Parity in case of serial connection + /// + public Parity Parity + { + get + { + if (serialport != null) + return parity; + else + return Parity.Even; + } + set + { + if (serialport != null) + parity = value; + } + } + + + /// + /// Gets or Sets the number of stopbits in case of serial connection + /// + public StopBits StopBits + { + get + { + if (serialport != null) + return stopBits; + else + return StopBits.One; + } + set + { + if (serialport != null) + stopBits = value; + } + } + + /// + /// Gets or Sets the connection Timeout in case of ModbusTCP connection + /// + public int ConnectionTimeout + { + get + { + return connectTimeout; + } + set + { + connectTimeout = value; + } + } + + /// + /// Gets or Sets the serial Port + /// + public string SerialPort + { + get + { + + return serialport.PortName; + } + set + { + if (value == null) + { + serialport = null; + return; + } + if (serialport != null) + serialport.Close(); + this.serialport = new SerialPort(); + this.serialport.PortName = value; + serialport.BaudRate = baudRate; + serialport.Parity = parity; + serialport.StopBits = stopBits; + serialport.WriteTimeout = 10000; + serialport.ReadTimeout = connectTimeout; + serialport.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); + } + } + + /// + /// Gets or Sets the Filename for the LogFile + /// + public string LogFileFilename + { + get + { + return StoreLogData.Instance.Filename; + } + set + { + StoreLogData.Instance.Filename = value; + if (StoreLogData.Instance.Filename != null) + debug = true; + else + debug = false; + } + } + + } +} diff --git a/EasyModbus_Net60/ModbusServer.cs b/EasyModbus_Net60/ModbusServer.cs new file mode 100644 index 0000000..13374cd --- /dev/null +++ b/EasyModbus_Net60/ModbusServer.cs @@ -0,0 +1,2266 @@ +/* +Copyright (c) 2018-2020 Rossmann-Engineering +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software +and associated documentation files (the "Software"), +to deal in the Software without restriction, +including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission +notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net.Sockets; +using System.Net; +using System.Threading; +using System.Net.NetworkInformation; +using System.IO.Ports; + +namespace EasyModbus +{ +#region class ModbusProtocol + /// + /// Modbus Protocol informations. + /// + public class ModbusProtocol + { + public enum ProtocolType { ModbusTCP = 0, ModbusUDP = 1, ModbusRTU = 2}; + public DateTime timeStamp; + public bool request; + public bool response; + public UInt16 transactionIdentifier; + public UInt16 protocolIdentifier; + public UInt16 length; + public byte unitIdentifier; + public byte functionCode; + public UInt16 startingAdress; + public UInt16 startingAddressRead; + public UInt16 startingAddressWrite; + public UInt16 quantity; + public UInt16 quantityRead; + public UInt16 quantityWrite; + public byte byteCount; + public byte exceptionCode; + public byte errorCode; + public UInt16[] receiveCoilValues; + public UInt16[] receiveRegisterValues; + public Int16[] sendRegisterValues; + public bool[] sendCoilValues; + public UInt16 crc; + } +#endregion + +#region structs + struct NetworkConnectionParameter + { + public NetworkStream stream; //For TCP-Connection only + public Byte[] bytes; + public int portIn; //For UDP-Connection only + public IPAddress ipAddressIn; //For UDP-Connection only + } +#endregion + +#region TCPHandler class + internal class TCPHandler + { + public delegate void DataChanged(object networkConnectionParameter); + public event DataChanged dataChanged; + + public delegate void NumberOfClientsChanged(); + public event NumberOfClientsChanged numberOfClientsChanged; + + TcpListener server = null; + + + private List tcpClientLastRequestList = new List(); + + public int NumberOfConnectedClients { get; set; } + + public string ipAddress = null; + + /// When making a server TCP listen socket, will listen to this IP address. + public IPAddress LocalIPAddress { + get { return localIPAddress; } + } + private IPAddress localIPAddress = IPAddress.Any; + + /// + /// Listen to all network interfaces. + /// + /// TCP port to listen + public TCPHandler(int port) + { + server = new TcpListener(LocalIPAddress, port); + server.Start(); + server.BeginAcceptTcpClient(AcceptTcpClientCallback, null); + } + + /// + /// Listen to a specific network interface. + /// + /// IP address of network interface to listen + /// TCP port to listen + public TCPHandler(IPAddress localIPAddress, int port) + { + this.localIPAddress = localIPAddress; + server = new TcpListener(LocalIPAddress, port); + server.Start(); + server.BeginAcceptTcpClient(AcceptTcpClientCallback, null); + } + + + private void AcceptTcpClientCallback(IAsyncResult asyncResult) + { + TcpClient tcpClient = new TcpClient(); + try + { + tcpClient = server.EndAcceptTcpClient(asyncResult); + tcpClient.ReceiveTimeout = 4000; + if (ipAddress != null) + { + string ipEndpoint = tcpClient.Client.RemoteEndPoint.ToString(); + ipEndpoint = ipEndpoint.Split(':')[0]; + if (ipEndpoint != ipAddress) + { + tcpClient.Client.Disconnect(false); + return; + } + } + } + catch (Exception) { } + try + { + server.BeginAcceptTcpClient(AcceptTcpClientCallback, null); + Client client = new Client(tcpClient); + NetworkStream networkStream = client.NetworkStream; + networkStream.ReadTimeout = 4000; + networkStream.BeginRead(client.Buffer, 0, client.Buffer.Length, ReadCallback, client); + } + catch (Exception) { } + } + + private int GetAndCleanNumberOfConnectedClients(Client client) + { + lock (this) + { + int i = 0; + bool objetExists = false; + foreach (Client clientLoop in tcpClientLastRequestList) + { + if (client.Equals(clientLoop)) + objetExists = true; + } + try + { + tcpClientLastRequestList.RemoveAll(delegate (Client c) + { + return ((DateTime.Now.Ticks - c.Ticks) > 40000000); + } + + ); + } + catch (Exception) { } + if (!objetExists) + tcpClientLastRequestList.Add(client); + + + return tcpClientLastRequestList.Count; + } + } + + private void ReadCallback(IAsyncResult asyncResult) + { + NetworkConnectionParameter networkConnectionParameter = new NetworkConnectionParameter(); + Client client = asyncResult.AsyncState as Client; + client.Ticks = DateTime.Now.Ticks; + NumberOfConnectedClients = GetAndCleanNumberOfConnectedClients(client); + if (numberOfClientsChanged != null) + numberOfClientsChanged(); + if (client != null) + { + int read; + NetworkStream networkStream = null; + try + { + networkStream = client.NetworkStream; + + read = networkStream.EndRead(asyncResult); + } + catch (Exception ex) + { + return; + } + + + if (read == 0) + { + //OnClientDisconnected(client.TcpClient); + //connectedClients.Remove(client); + return; + } + byte[] data = new byte[read]; + Buffer.BlockCopy(client.Buffer, 0, data, 0, read); + networkConnectionParameter.bytes = data; + networkConnectionParameter.stream = networkStream; + if (dataChanged != null) + dataChanged(networkConnectionParameter); + try + { + networkStream.BeginRead(client.Buffer, 0, client.Buffer.Length, ReadCallback, client); + } + catch (Exception) + { + } + } + } + + public void Disconnect() + { + try + { + foreach (Client clientLoop in tcpClientLastRequestList) + { + clientLoop.NetworkStream.Close(00); + } + } + catch (Exception) { } + server.Stop(); + + } + + + internal class Client + { + private readonly TcpClient tcpClient; + private readonly byte[] buffer; + public long Ticks { get; set; } + + public Client(TcpClient tcpClient) + { + this.tcpClient = tcpClient; + int bufferSize = tcpClient.ReceiveBufferSize; + buffer = new byte[bufferSize]; + } + + public TcpClient TcpClient + { + get { return tcpClient; } + } + + public byte[] Buffer + { + get { return buffer; } + } + + public NetworkStream NetworkStream + { + get { + + return tcpClient.GetStream(); + + } + } + } + } +#endregion + + /// + /// Modbus TCP Server. + /// + public class ModbusServer + { + private bool debug = false; + Int32 port = 502; + ModbusProtocol receiveData; + ModbusProtocol sendData = new ModbusProtocol(); + Byte[] bytes = new Byte[2100]; + //public Int16[] _holdingRegisters = new Int16[65535]; + public HoldingRegisters holdingRegisters; + public InputRegisters inputRegisters; + public Coils coils; + public DiscreteInputs discreteInputs; + private int numberOfConnections = 0; + private bool udpFlag; + private bool serialFlag; + private int baudrate = 9600; + private System.IO.Ports.Parity parity = Parity.Even; + private System.IO.Ports.StopBits stopBits = StopBits.One; + private string serialPort = "COM1"; + private SerialPort serialport; + private byte unitIdentifier = 1; + private int portIn; + private IPAddress ipAddressIn; + private UdpClient udpClient; + private IPEndPoint iPEndPoint; + private TCPHandler tcpHandler; + Thread listenerThread; + Thread clientConnectionThread; + private ModbusProtocol[] modbusLogData = new ModbusProtocol[100]; + public bool FunctionCode1Disabled {get; set;} + public bool FunctionCode2Disabled { get; set; } + public bool FunctionCode3Disabled { get; set; } + public bool FunctionCode4Disabled { get; set; } + public bool FunctionCode5Disabled { get; set; } + public bool FunctionCode6Disabled { get; set; } + public bool FunctionCode15Disabled { get; set; } + public bool FunctionCode16Disabled { get; set; } + public bool FunctionCode23Disabled { get; set; } + public bool PortChanged { get; set; } + object lockCoils = new object(); + object lockHoldingRegisters = new object(); + private volatile bool shouldStop; + + private IPAddress localIPAddress = IPAddress.Any; + + /// + /// When creating a TCP or UDP socket, the local IP address to attach to. + /// + public IPAddress LocalIPAddress + { + get { return localIPAddress; } + set { if (listenerThread == null) localIPAddress = value; } + } + + public ModbusServer() + { + holdingRegisters = new HoldingRegisters(this); + inputRegisters = new InputRegisters(this); + coils = new Coils(this); + discreteInputs = new DiscreteInputs(this); + + } + + #region events + public delegate void CoilsChangedHandler(int coil, int numberOfCoils); + public event CoilsChangedHandler CoilsChanged; + + public delegate void HoldingRegistersChangedHandler(int register, int numberOfRegisters); + public event HoldingRegistersChangedHandler HoldingRegistersChanged; + + public delegate void NumberOfConnectedClientsChangedHandler(); + public event NumberOfConnectedClientsChangedHandler NumberOfConnectedClientsChanged; + + public delegate void LogDataChangedHandler(); + public event LogDataChangedHandler LogDataChanged; + #endregion + + public void Listen() + { + + listenerThread = new Thread(ListenerThread); + listenerThread.Start(); + } + + public void StopListening() + { + if (SerialFlag & (serialport != null)) + { + if (serialport.IsOpen) + serialport.Close(); + shouldStop = true; + } + try + { + tcpHandler.Disconnect(); + listenerThread.Abort(); + + } + catch (Exception) { } + listenerThread.Join(); + try + { + + clientConnectionThread.Abort(); + } + catch (Exception) { } + } + + private void ListenerThread() + { + if (!udpFlag & !serialFlag) + { + if (udpClient != null) + { + try + { + udpClient.Close(); + } + catch (Exception) { } + } + tcpHandler = new TCPHandler(LocalIPAddress, port); + if (debug) StoreLogData.Instance.Store($"EasyModbus Server listing for incomming data at Port {port}, local IP {LocalIPAddress}", System.DateTime.Now); + tcpHandler.dataChanged += new TCPHandler.DataChanged(ProcessReceivedData); + tcpHandler.numberOfClientsChanged += new TCPHandler.NumberOfClientsChanged(numberOfClientsChanged); + } + else if (serialFlag) + { + if (serialport == null) + { + if (debug) StoreLogData.Instance.Store("EasyModbus RTU-Server listing for incomming data at Serial Port " + serialPort, System.DateTime.Now); + serialport = new SerialPort(); + serialport.PortName = serialPort; + serialport.BaudRate = this.baudrate; + serialport.Parity = this.parity; + serialport.StopBits = stopBits; + serialport.WriteTimeout = 10000; + serialport.ReadTimeout = 1000; + serialport.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); + serialport.Open(); + } + } + else + while (!shouldStop) + { + if (udpFlag) + { + if (udpClient == null | PortChanged) + { + IPEndPoint localEndoint = new IPEndPoint(LocalIPAddress, port); + udpClient = new UdpClient(localEndoint); + if (debug) StoreLogData.Instance.Store($"EasyModbus Server listing for incomming data at Port {port}, local IP {LocalIPAddress}", System.DateTime.Now); + udpClient.Client.ReceiveTimeout = 1000; + iPEndPoint = new IPEndPoint(IPAddress.Any, port); + PortChanged = false; + } + if (tcpHandler != null) + tcpHandler.Disconnect(); + try + { + bytes = udpClient.Receive(ref iPEndPoint); + portIn = iPEndPoint.Port; + NetworkConnectionParameter networkConnectionParameter = new NetworkConnectionParameter(); + networkConnectionParameter.bytes = bytes; + ipAddressIn = iPEndPoint.Address; + networkConnectionParameter.portIn = portIn; + networkConnectionParameter.ipAddressIn = ipAddressIn; + ParameterizedThreadStart pts = new ParameterizedThreadStart(this.ProcessReceivedData); + Thread processDataThread = new Thread(pts); + processDataThread.Start(networkConnectionParameter); + } + catch (Exception) + { + } + } + + } + } + + #region SerialHandler + private bool dataReceived = false; + private byte[] readBuffer = new byte[2094]; + private DateTime lastReceive; + private int nextSign = 0; + private void DataReceivedHandler(object sender, + SerialDataReceivedEventArgs e) + { + int silence = 4000 / baudrate; + if ((DateTime.Now.Ticks - lastReceive.Ticks) > TimeSpan.TicksPerMillisecond*silence) + nextSign = 0; + + + SerialPort sp = (SerialPort)sender; + + int numbytes = sp.BytesToRead; + byte[] rxbytearray = new byte[numbytes]; + + sp.Read(rxbytearray, 0, numbytes); + + Array.Copy(rxbytearray, 0, readBuffer, nextSign, rxbytearray.Length); + lastReceive= DateTime.Now; + nextSign = numbytes+ nextSign; + if (ModbusClient.DetectValidModbusFrame(readBuffer, nextSign)) + { + + dataReceived = true; + nextSign= 0; + + NetworkConnectionParameter networkConnectionParameter = new NetworkConnectionParameter(); + networkConnectionParameter.bytes = readBuffer; + ParameterizedThreadStart pts = new ParameterizedThreadStart(this.ProcessReceivedData); + Thread processDataThread = new Thread(pts); + processDataThread.Start(networkConnectionParameter); + dataReceived = false; + + } + else + dataReceived = false; + } + #endregion + + #region Method numberOfClientsChanged + private void numberOfClientsChanged() + { + numberOfConnections = tcpHandler.NumberOfConnectedClients; + if (NumberOfConnectedClientsChanged != null) + NumberOfConnectedClientsChanged(); + } + #endregion + + object lockProcessReceivedData = new object(); + #region Method ProcessReceivedData + private void ProcessReceivedData(object networkConnectionParameter) + { + lock (lockProcessReceivedData) + { + Byte[] bytes = new byte[((NetworkConnectionParameter)networkConnectionParameter).bytes.Length]; + if (debug) StoreLogData.Instance.Store("Received Data: " + BitConverter.ToString(bytes), System.DateTime.Now); + NetworkStream stream = ((NetworkConnectionParameter)networkConnectionParameter).stream; + int portIn = ((NetworkConnectionParameter)networkConnectionParameter).portIn; + IPAddress ipAddressIn = ((NetworkConnectionParameter)networkConnectionParameter).ipAddressIn; + + + Array.Copy(((NetworkConnectionParameter)networkConnectionParameter).bytes, 0, bytes, 0, ((NetworkConnectionParameter)networkConnectionParameter).bytes.Length); + + ModbusProtocol receiveDataThread = new ModbusProtocol(); + ModbusProtocol sendDataThread = new ModbusProtocol(); + + try + { + UInt16[] wordData = new UInt16[1]; + byte[] byteData = new byte[2]; + receiveDataThread.timeStamp = DateTime.Now; + receiveDataThread.request = true; + if (!serialFlag) + { + //Lese Transaction identifier + byteData[1] = bytes[0]; + byteData[0] = bytes[1]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.transactionIdentifier = wordData[0]; + + //Lese Protocol identifier + byteData[1] = bytes[2]; + byteData[0] = bytes[3]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.protocolIdentifier = wordData[0]; + + //Lese length + byteData[1] = bytes[4]; + byteData[0] = bytes[5]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.length = wordData[0]; + } + + //Lese unit identifier + receiveDataThread.unitIdentifier = bytes[6 - 6 * Convert.ToInt32(serialFlag)]; + //Check UnitIdentifier + if ((receiveDataThread.unitIdentifier != this.unitIdentifier) & (receiveDataThread.unitIdentifier != 0)) + return; + + // Lese function code + receiveDataThread.functionCode = bytes[7 - 6 * Convert.ToInt32(serialFlag)]; + + // Lese starting address + byteData[1] = bytes[8 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[9 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.startingAdress = wordData[0]; + + if (receiveDataThread.functionCode <= 4) + { + // Lese quantity + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.quantity = wordData[0]; + } + if (receiveDataThread.functionCode == 5) + { + receiveDataThread.receiveCoilValues = new ushort[1]; + // Lese Value + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, receiveDataThread.receiveCoilValues, 0, 2); + } + if (receiveDataThread.functionCode == 6) + { + receiveDataThread.receiveRegisterValues = new ushort[1]; + // Lese Value + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, receiveDataThread.receiveRegisterValues, 0, 2); + } + if (receiveDataThread.functionCode == 15) + { + // Lese quantity + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.quantity = wordData[0]; + + receiveDataThread.byteCount = bytes[12 - 6 * Convert.ToInt32(serialFlag)]; + + if ((receiveDataThread.byteCount % 2) != 0) + receiveDataThread.receiveCoilValues = new ushort[receiveDataThread.byteCount / 2 + 1]; + else + receiveDataThread.receiveCoilValues = new ushort[receiveDataThread.byteCount / 2]; + // Lese Value + Buffer.BlockCopy(bytes, 13 - 6 * Convert.ToInt32(serialFlag), receiveDataThread.receiveCoilValues, 0, receiveDataThread.byteCount); + } + if (receiveDataThread.functionCode == 16) + { + // Lese quantity + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.quantity = wordData[0]; + + receiveDataThread.byteCount = bytes[12 - 6 * Convert.ToInt32(serialFlag)]; + receiveDataThread.receiveRegisterValues = new ushort[receiveDataThread.quantity]; + for (int i = 0; i < receiveDataThread.quantity; i++) + { + // Lese Value + byteData[1] = bytes[13 + i * 2 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[14 + i * 2 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, receiveDataThread.receiveRegisterValues, i * 2, 2); + } + + } + if (receiveDataThread.functionCode == 23) + { + // Lese starting Address Read + byteData[1] = bytes[8 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[9 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.startingAddressRead = wordData[0]; + // Lese quantity Read + byteData[1] = bytes[10 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[11 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.quantityRead = wordData[0]; + // Lese starting Address Write + byteData[1] = bytes[12 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[13 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.startingAddressWrite = wordData[0]; + // Lese quantity Write + byteData[1] = bytes[14 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[15 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, wordData, 0, 2); + receiveDataThread.quantityWrite = wordData[0]; + + receiveDataThread.byteCount = bytes[16 - 6 * Convert.ToInt32(serialFlag)]; + receiveDataThread.receiveRegisterValues = new ushort[receiveDataThread.quantityWrite]; + for (int i = 0; i < receiveDataThread.quantityWrite; i++) + { + // Lese Value + byteData[1] = bytes[17 + i * 2 - 6 * Convert.ToInt32(serialFlag)]; + byteData[0] = bytes[18 + i * 2 - 6 * Convert.ToInt32(serialFlag)]; + Buffer.BlockCopy(byteData, 0, receiveDataThread.receiveRegisterValues, i * 2, 2); + } + } + } + catch (Exception exc) + { } + this.CreateAnswer(receiveDataThread, sendDataThread, stream, portIn, ipAddressIn); + //this.sendAnswer(); + this.CreateLogData(receiveDataThread, sendDataThread); + + if (LogDataChanged != null) + LogDataChanged(); + } + } + #endregion + + #region Method CreateAnswer + private void CreateAnswer(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + + switch (receiveData.functionCode) + { + // Read Coils + case 1: + if (!FunctionCode1Disabled) + this.ReadCoils(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + break; + // Read Input Registers + case 2: + if (!FunctionCode2Disabled) + this.ReadDiscreteInputs(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Read Holding Registers + case 3: + if (!FunctionCode3Disabled) + this.ReadHoldingRegisters(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Read Input Registers + case 4: + if (!FunctionCode4Disabled) + this.ReadInputRegisters(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Write single coil + case 5: + if (!FunctionCode5Disabled) + this.WriteSingleCoil(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Write single register + case 6: + if (!FunctionCode6Disabled) + this.WriteSingleRegister(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Write Multiple coils + case 15: + if (!FunctionCode15Disabled) + this.WriteMultipleCoils(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Write Multiple registers + case 16: + if (!FunctionCode16Disabled) + this.WriteMultipleRegisters(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Error: Function Code not supported + case 23: + if (!FunctionCode23Disabled) + this.ReadWriteMultipleRegisters(receiveData, sendData, stream, portIn, ipAddressIn); + else + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + } + + break; + // Error: Function Code not supported + default: sendData.errorCode = (byte) (receiveData.functionCode + 0x80); + sendData.exceptionCode = 1; + sendException(sendData.errorCode, sendData.exceptionCode, receiveData, sendData, stream, portIn, ipAddressIn); + break; + } + sendData.timeStamp = DateTime.Now; + } + #endregion + + private void ReadCoils(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + if ((receiveData.quantity < 1) | (receiveData.quantity > 0x07D0)) //Invalid quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1 + receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + if ((receiveData.quantity % 8) == 0) + sendData.byteCount = (byte)(receiveData.quantity / 8); + else + sendData.byteCount = (byte)(receiveData.quantity / 8 + 1); + + sendData.sendCoilValues = new bool[receiveData.quantity]; + lock (lockCoils) + Array.Copy(coils.localArray, receiveData.startingAdress + 1, sendData.sendCoilValues, 0, receiveData.quantity); + } + if (true) + { + Byte[] data; + + if (sendData.exceptionCode > 0) + data = new byte[9 + 2*Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount+ 2*Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + //ByteCount + data[8] = sendData.byteCount; + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendCoilValues = null; + } + + if (sendData.sendCoilValues != null) + for (int i = 0; i < (sendData.byteCount); i++) + { + byteData = new byte[2]; + for (int j = 0; j < 8; j++) + { + + byte boolValue; + if (sendData.sendCoilValues[i * 8 + j] == true) + boolValue = 1; + else + boolValue = 0; + byteData[1] = (byte)((byteData[1]) | (boolValue << j)); + if ((i * 8 + j + 1) >= sendData.sendCoilValues.Length) + break; + } + data[9 + i] = byteData[1]; + } + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + } + } + + private void ReadDiscreteInputs(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + if ((receiveData.quantity < 1) | (receiveData.quantity > 0x07D0)) //Invalid quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1 + receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + if ((receiveData.quantity % 8) == 0) + sendData.byteCount = (byte)(receiveData.quantity / 8); + else + sendData.byteCount = (byte)(receiveData.quantity / 8 + 1); + + sendData.sendCoilValues = new bool[receiveData.quantity]; + Array.Copy(discreteInputs.localArray, receiveData.startingAdress + 1, sendData.sendCoilValues, 0, receiveData.quantity); + } + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount + 2 * Convert.ToInt32(serialFlag)]; + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + //ByteCount + data[8] = sendData.byteCount; + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendCoilValues = null; + } + + if (sendData.sendCoilValues != null) + for (int i = 0; i < (sendData.byteCount); i++) + { + byteData = new byte[2]; + for (int j = 0; j < 8; j++) + { + + byte boolValue; + if (sendData.sendCoilValues[i * 8 + j] == true) + boolValue = 1; + else + boolValue = 0; + byteData[1] = (byte)((byteData[1]) | (boolValue << j)); + if ((i * 8 + j + 1) >= sendData.sendCoilValues.Length) + break; + } + data[9 + i] = byteData[1]; + } + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if(debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + } + } + + private void ReadHoldingRegisters(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + if ((receiveData.quantity < 1) | (receiveData.quantity > 0x007D)) //Invalid quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1 + receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + sendData.byteCount = (byte)(2 * receiveData.quantity); + sendData.sendRegisterValues = new Int16[receiveData.quantity]; + lock (lockHoldingRegisters) + Buffer.BlockCopy(holdingRegisters.localArray, receiveData.startingAdress * 2 + 2, sendData.sendRegisterValues, 0, receiveData.quantity * 2); + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = (ushort)(0x03 + sendData.byteCount); + + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount + 2 * Convert.ToInt32(serialFlag)]; + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + //ByteCount + data[8] = sendData.byteCount; + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + + + if (sendData.sendRegisterValues != null) + for (int i = 0; i < (sendData.byteCount / 2); i++) + { + byteData = BitConverter.GetBytes((Int16)sendData.sendRegisterValues[i]); + data[9 + i * 2] = byteData[1]; + data[10 + i * 2] = byteData[0]; + } + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + } + } + + private void ReadInputRegisters(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + if ((receiveData.quantity < 1) | (receiveData.quantity > 0x007D)) //Invalid quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1 + receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + sendData.byteCount = (byte)(2 * receiveData.quantity); + sendData.sendRegisterValues = new Int16[receiveData.quantity]; + Buffer.BlockCopy(inputRegisters.localArray, receiveData.startingAdress * 2 + 2, sendData.sendRegisterValues, 0, receiveData.quantity * 2); + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = (ushort)(0x03 + sendData.byteCount); + + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount + 2 * Convert.ToInt32(serialFlag)]; + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + //ByteCount + data[8] = sendData.byteCount; + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + + + if (sendData.sendRegisterValues != null) + for (int i = 0; i < (sendData.byteCount / 2); i++) + { + byteData = BitConverter.GetBytes((Int16)sendData.sendRegisterValues[i]); + data[9 + i * 2] = byteData[1]; + data[10 + i * 2] = byteData[0]; + } + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + } + } + + private void WriteSingleCoil(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + sendData.startingAdress = receiveData.startingAdress; + sendData.receiveCoilValues = receiveData.receiveCoilValues; + if ((receiveData.receiveCoilValues[0] != 0x0000) & (receiveData.receiveCoilValues[0] != 0xFF00)) //Invalid Value + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + if (receiveData.receiveCoilValues[0] == 0xFF00) + { + lock (lockCoils) + coils[receiveData.startingAdress + 1] = true; + } + if (receiveData.receiveCoilValues[0] == 0x0000) + { + lock (lockCoils) + coils[receiveData.startingAdress + 1] = false; + } + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = 0x06; + + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[12 + 2 * Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + else + { + byteData = BitConverter.GetBytes((int)receiveData.startingAdress); + data[8] = byteData[1]; + data[9] = byteData[0]; + byteData = BitConverter.GetBytes((int)receiveData.receiveCoilValues[0]); + data[10] = byteData[1]; + data[11] = byteData[0]; + } + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + if (CoilsChanged != null) + CoilsChanged(receiveData.startingAdress+1, 1); + } + } + + private void WriteSingleRegister(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + sendData.startingAdress = receiveData.startingAdress; + sendData.receiveRegisterValues = receiveData.receiveRegisterValues; + + if ((receiveData.receiveRegisterValues[0] < 0x0000) | (receiveData.receiveRegisterValues[0] > 0xFFFF)) //Invalid Value + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if (((receiveData.startingAdress + 1) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + lock (lockHoldingRegisters) + holdingRegisters[receiveData.startingAdress + 1] = unchecked((short)receiveData.receiveRegisterValues[0]); + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = 0x06; + + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[12 + 2 * Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + else + { + byteData = BitConverter.GetBytes((int)receiveData.startingAdress); + data[8] = byteData[1]; + data[9] = byteData[0]; + byteData = BitConverter.GetBytes((int)receiveData.receiveRegisterValues[0]); + data[10] = byteData[1]; + data[11] = byteData[0]; + } + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + if (HoldingRegistersChanged != null) + HoldingRegistersChanged(receiveData.startingAdress+1, 1); + } + } + + private void WriteMultipleCoils(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + sendData.startingAdress = receiveData.startingAdress; + sendData.quantity = receiveData.quantity; + + if ((receiveData.quantity == 0x0000) | (receiveData.quantity > 0x07B0)) //Invalid Quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if ((((int)receiveData.startingAdress + 1 + (int)receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + lock (lockCoils) + for (int i = 0; i < receiveData.quantity; i++) + { + int shift = i % 16; + /* if ((i == receiveData.quantity - 1) & (receiveData.quantity % 2 != 0)) + { + if (shift < 8) + shift = shift + 8; + else + shift = shift - 8; + }*/ + int mask = 0x1; + mask = mask << (shift); + if ((receiveData.receiveCoilValues[i / 16] & (ushort)mask) == 0) + + coils[receiveData.startingAdress + i + 1] = false; + else + + coils[receiveData.startingAdress + i + 1] = true; + + } + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = 0x06; + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[12 + 2 * Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + else + { + byteData = BitConverter.GetBytes((int)receiveData.startingAdress); + data[8] = byteData[1]; + data[9] = byteData[0]; + byteData = BitConverter.GetBytes((int)receiveData.quantity); + data[10] = byteData[1]; + data[11] = byteData[0]; + } + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + if (CoilsChanged != null) + CoilsChanged(receiveData.startingAdress+1, receiveData.quantity); + } + } + + private void WriteMultipleRegisters(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + sendData.startingAdress = receiveData.startingAdress; + sendData.quantity = receiveData.quantity; + + if ((receiveData.quantity == 0x0000) | (receiveData.quantity > 0x07B0)) //Invalid Quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if ((((int)receiveData.startingAdress + 1 + (int)receiveData.quantity) > 65535) | (receiveData.startingAdress < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + lock (lockHoldingRegisters) + for (int i = 0; i < receiveData.quantity; i++) + { + holdingRegisters[receiveData.startingAdress + i + 1] = unchecked((short)receiveData.receiveRegisterValues[i]); + } + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = 0x06; + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[12 + 2 * Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + else + { + byteData = BitConverter.GetBytes((int)receiveData.startingAdress); + data[8] = byteData[1]; + data[9] = byteData[0]; + byteData = BitConverter.GetBytes((int)receiveData.quantity); + data[10] = byteData[1]; + data[11] = byteData[0]; + } + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + if (HoldingRegistersChanged != null) + HoldingRegistersChanged(receiveData.startingAdress+1, receiveData.quantity); + } + } + + private void ReadWriteMultipleRegisters(ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = this.unitIdentifier; + sendData.functionCode = receiveData.functionCode; + + + if ((receiveData.quantityRead < 0x0001) | (receiveData.quantityRead > 0x007D) | (receiveData.quantityWrite < 0x0001) | (receiveData.quantityWrite > 0x0079) | (receiveData.byteCount != (receiveData.quantityWrite * 2))) //Invalid Quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 3; + } + if ((((int)receiveData.startingAddressRead + 1 + (int)receiveData.quantityRead) > 65535) | (((int)receiveData.startingAddressWrite + 1 + (int)receiveData.quantityWrite) > 65535) | (receiveData.quantityWrite < 0) | (receiveData.quantityRead < 0)) //Invalid Starting adress or Starting address + quantity + { + sendData.errorCode = (byte)(receiveData.functionCode + 0x80); + sendData.exceptionCode = 2; + } + if (sendData.exceptionCode == 0) + { + sendData.sendRegisterValues = new Int16[receiveData.quantityRead]; + lock (lockHoldingRegisters) + Buffer.BlockCopy(holdingRegisters.localArray, receiveData.startingAddressRead * 2 + 2, sendData.sendRegisterValues, 0, receiveData.quantityRead * 2); + + lock (holdingRegisters) + for (int i = 0; i < receiveData.quantityWrite; i++) + { + holdingRegisters[receiveData.startingAddressWrite + i + 1] = unchecked((short)receiveData.receiveRegisterValues[i]); + } + sendData.byteCount = (byte)(2 * receiveData.quantityRead); + } + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = Convert.ToUInt16(3 + 2 * receiveData.quantityRead); + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount + 2 * Convert.ToInt32(serialFlag)]; + + Byte[] byteData = new byte[2]; + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + //Function Code + data[7] = sendData.functionCode; + + //ByteCount + data[8] = sendData.byteCount; + + + if (sendData.exceptionCode > 0) + { + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + sendData.sendRegisterValues = null; + } + else + { + if (sendData.sendRegisterValues != null) + for (int i = 0; i < (sendData.byteCount / 2); i++) + { + byteData = BitConverter.GetBytes((Int16)sendData.sendRegisterValues[i]); + data[9 + i * 2] = byteData[1]; + data[10 + i * 2] = byteData[0]; + } + + } + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + if (HoldingRegistersChanged != null) + HoldingRegistersChanged(receiveData.startingAddressWrite+1, receiveData.quantityWrite); + } + } + + private void sendException(int errorCode, int exceptionCode, ModbusProtocol receiveData, ModbusProtocol sendData, NetworkStream stream, int portIn, IPAddress ipAddressIn) + { + sendData.response = true; + + sendData.transactionIdentifier = receiveData.transactionIdentifier; + sendData.protocolIdentifier = receiveData.protocolIdentifier; + + sendData.unitIdentifier = receiveData.unitIdentifier; + sendData.errorCode = (byte)errorCode; + sendData.exceptionCode = (byte)exceptionCode; + + if (sendData.exceptionCode > 0) + sendData.length = 0x03; + else + sendData.length = (ushort)(0x03 + sendData.byteCount); + + if (true) + { + Byte[] data; + if (sendData.exceptionCode > 0) + data = new byte[9 + 2 * Convert.ToInt32(serialFlag)]; + else + data = new byte[9 + sendData.byteCount + 2 * Convert.ToInt32(serialFlag)]; + Byte[] byteData = new byte[2]; + sendData.length = (byte)(data.Length - 6); + + //Send Transaction identifier + byteData = BitConverter.GetBytes((int)sendData.transactionIdentifier); + data[0] = byteData[1]; + data[1] = byteData[0]; + + //Send Protocol identifier + byteData = BitConverter.GetBytes((int)sendData.protocolIdentifier); + data[2] = byteData[1]; + data[3] = byteData[0]; + + //Send length + byteData = BitConverter.GetBytes((int)sendData.length); + data[4] = byteData[1]; + data[5] = byteData[0]; + + //Unit Identifier + data[6] = sendData.unitIdentifier; + + + data[7] = sendData.errorCode; + data[8] = sendData.exceptionCode; + + + try + { + if (serialFlag) + { + if (!serialport.IsOpen) + throw new EasyModbus.Exceptions.SerialPortNotOpenedException("serial port not opened"); + //Create CRC + sendData.crc = ModbusClient.calculateCRC(data, Convert.ToUInt16(data.Length - 8), 6); + byteData = BitConverter.GetBytes((int)sendData.crc); + data[data.Length - 2] = byteData[0]; + data[data.Length - 1] = byteData[1]; + serialport.Write(data, 6, data.Length - 6); + if (debug) + { + byte[] debugData = new byte[data.Length - 6]; + Array.Copy(data, 6, debugData, 0, data.Length - 6); + if (debug) StoreLogData.Instance.Store("Send Serial-Data: " + BitConverter.ToString(debugData), System.DateTime.Now); + } + } + else if (udpFlag) + { + //UdpClient udpClient = new UdpClient(); + IPEndPoint endPoint = new IPEndPoint(ipAddressIn, portIn); + udpClient.Send(data, data.Length, endPoint); + + } + else + { + stream.Write(data, 0, data.Length); + if (debug) StoreLogData.Instance.Store("Send Data: " + BitConverter.ToString(data), System.DateTime.Now); + } + } + catch (Exception) { } + } + } + + private void CreateLogData(ModbusProtocol receiveData, ModbusProtocol sendData) + { + for (int i = 0; i < 98; i++) + { + modbusLogData[99 - i] = modbusLogData[99 - i - 2]; + + } + modbusLogData[0] = receiveData; + modbusLogData[1] = sendData; + + } + + + + public int NumberOfConnections + { + get + { + return numberOfConnections; + } + } + + public ModbusProtocol[] ModbusLogData + { + get + { + return modbusLogData; + } + } + + public int Port + { + get + { + return port; + } + set + { + port = value; + + + } + } + + public bool UDPFlag + { + get + { + return udpFlag; + } + set + { + udpFlag = value; + } + } + + public bool SerialFlag + { + get + { + return serialFlag; + } + set + { + serialFlag = value; + } + } + + public int Baudrate + { + get + { + return baudrate; + } + set + { + baudrate = value; + } + } + + public System.IO.Ports.Parity Parity + { + get + { + return parity; + } + set + { + parity = value; + } + } + + public System.IO.Ports.StopBits StopBits + { + get + { + return stopBits; + } + set + { + stopBits = value; + } + } + + public string SerialPort + { + get + { + return serialPort; + } + set + { + serialPort = value; + if (serialPort != null) + serialFlag = true; + else + serialFlag = false; + } + } + + public byte UnitIdentifier + { + get + { + return unitIdentifier; + } + set + { + unitIdentifier = value; + } + } + + + + + /// + /// Gets or Sets the Filename for the LogFile + /// + public string LogFileFilename + { + get + { + return StoreLogData.Instance.Filename; + } + set + { + StoreLogData.Instance.Filename = value; + if (StoreLogData.Instance.Filename != null) + debug = true; + else + debug = false; + } + } + + + + + public class HoldingRegisters + { + public Int16[] localArray = new Int16[65535]; + ModbusServer modbusServer; + + public HoldingRegisters(EasyModbus.ModbusServer modbusServer) + { + this.modbusServer = modbusServer; + } + + public Int16 this[int x] + { + get { return this.localArray[x]; } + set + { + this.localArray[x] = value; + + } + } + } + + public class InputRegisters + { + public Int16[] localArray = new Int16[65535]; + ModbusServer modbusServer; + + public InputRegisters(EasyModbus.ModbusServer modbusServer) + { + this.modbusServer = modbusServer; + } + + public Int16 this[int x] + { + get { return this.localArray[x]; } + set + { + this.localArray[x] = value; + + } + } + } + + public class Coils + { + public bool[] localArray = new bool[65535]; + ModbusServer modbusServer; + + public Coils(EasyModbus.ModbusServer modbusServer) + { + this.modbusServer = modbusServer; + } + + public bool this[int x] + { + get { return this.localArray[x]; } + set + { + this.localArray[x] = value; + + } + } + } + + public class DiscreteInputs + { + public bool[] localArray = new bool[65535]; + ModbusServer modbusServer; + + public DiscreteInputs(EasyModbus.ModbusServer modbusServer) + { + this.modbusServer = modbusServer; + } + + public bool this[int x] + { + get { return this.localArray[x]; } + set + { + this.localArray[x] = value; + + } + } + + + } + } +} + \ No newline at end of file diff --git a/EasyModbus_Net60/StoreLogData.cs b/EasyModbus_Net60/StoreLogData.cs new file mode 100644 index 0000000..3345038 --- /dev/null +++ b/EasyModbus_Net60/StoreLogData.cs @@ -0,0 +1,120 @@ +/* +Copyright (c) 2018-2020 Rossmann-Engineering +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software +and associated documentation files (the "Software"), +to deal in the Software without restriction, +including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission +notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace EasyModbus +{ + /// + /// Store Log-Data in a File + /// + public sealed class StoreLogData + { + private String filename = null; + private static volatile StoreLogData instance; + private static object syncObject = new Object(); + + /// + /// Private constructor; Ensures the access of the class only via "instance" + /// + private StoreLogData() + { + } + + /// + /// Returns the instance of the class (singleton) + /// + /// instance (Singleton) + public static StoreLogData Instance + { + get + { + if (instance == null) + { + lock (syncObject) + { + if (instance == null) + instance = new StoreLogData(); + } + } + + return instance; + } + } + + /// + /// Store message in Log-File + /// + /// Message to append to the Log-File + public void Store(String message) + { + if (this.filename == null) + return; + + using (System.IO.StreamWriter file = + new System.IO.StreamWriter(Filename, true)) + { + file.WriteLine(message); + } + } + + /// + /// Store message in Log-File including Timestamp + /// + /// Message to append to the Log-File + /// Timestamp to add to the same Row + public void Store(String message, DateTime timestamp) + { + try + { + using (System.IO.StreamWriter file = + new System.IO.StreamWriter(Filename, true)) + { + file.WriteLine(timestamp.ToString("dd.MM.yyyy H:mm:ss.ff ") + message); + } + } + catch (Exception e) + { + + } + } + + /// + /// Gets or Sets the Filename to Store Strings in a File + /// + public string Filename + { + get + { + return filename; + } + set + { + filename = value; + } + } + } +}