/* ANY-LICENSE V1.0 ---------------- You can use these files under any license approved by the Open Source Initiative, preferrably one of the popular licenses, as long as the license you choose is compatible to the dependencies of these files. See http://www.opensource.org/licenses/ for a list of approved licenses. Author: Martin Furter Project: borgnet, borg.ch C# classes and examples. Modified: 2019 Support for atomically replacing an existing file. */ using BorgCh; using BorgCh.Comm; using System; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Net; using System.Net.Sockets; using System.Threading; namespace BorgCh.Comm.Modbus { // Errors returned by the modbus functions. public enum ModbusError { // no conection? NO_CONNECTION, // Invalid slave ID (zero). INVALID_SLAVE_ID, // Not enough registers (zero) requested. TOO_FEW_REGISTERS, // Too many registers requested. TOO_MANY_REGISTERS, // An invalid offset was specified. INVALID_OFFSET, // Last requested register is too high. LAST_REG_NR_TOO_HIGH, // Failed to open serial port. SERPORT_OPEN_ERROR, // Failed to connect TCP socket. TCP_CONNECT_ERROR, // Error sending data. SEND_ERROR, // Error receiving data. RECV_ERROR, // Received too short reply. RECV_SHORT_REPLY, // Timeout waiting for a reply. TIMEOUT, // Received different slave ID in reply. UNIT_ID_MISMATCH, // Received invalid function in reply. FUNCTION_MISMATCH, // Received invalid checksum. CRC_MISMATCH, // Slave replied with error code 'illegal function'. ILLEGAL_FUNCTION, // Slave replied with error code 'illegal data address'. ILLEGAL_DATA_ADDRESS, // Slave replied with error code 'illegal data value'. ILLEGAL_DATA_VALUE, // Slave replied with error code 'device failure'. SLAVE_DEVICE_FAILURE, // Slave replied with error code 'device busy SLAVE_DEVICE_BUSY, // The targeted device failed to respond. // The gateway generates this exception NO_RESPONSE_FROM_TARGET, // Slave replied with unknown error code. UNKNOWN_SLAVE_ERROR, // A communication error occurred. COMMUNICATION_ERROR, // Received an error reply. //ERROR_REPLY, // An internal error occurred. INTERNAL_ERROR, // OK, no error. OK } public class ModbusErr { public static string ToString( ModbusError err ) { switch( err ) { case ModbusError.NO_CONNECTION: return "No connection"; case ModbusError.INVALID_SLAVE_ID: return "Invalid slave ID"; case ModbusError.TOO_FEW_REGISTERS: return "Too few registers requested"; case ModbusError.TOO_MANY_REGISTERS: return "Too many registers requested"; case ModbusError.LAST_REG_NR_TOO_HIGH: return "Last register number too high"; case ModbusError.SERPORT_OPEN_ERROR: return "Serial port open error"; case ModbusError.TCP_CONNECT_ERROR: return "TCP connect error"; case ModbusError.SEND_ERROR: return "Send error"; case ModbusError.RECV_ERROR: return "Recv error"; case ModbusError.RECV_SHORT_REPLY: return "Received short reply"; case ModbusError.TIMEOUT: return "Timeout"; case ModbusError.UNIT_ID_MISMATCH: return "Unit ID mismatch in reply"; case ModbusError.FUNCTION_MISMATCH: return "Function mismatch in reply"; case ModbusError.CRC_MISMATCH: return "CRC mismatch in reply"; case ModbusError.ILLEGAL_FUNCTION: return "Device replied with 'illegal function'"; case ModbusError.ILLEGAL_DATA_ADDRESS: return "Device replied with 'illegal data address'"; case ModbusError.ILLEGAL_DATA_VALUE: return "Device replied with 'illegal data value'"; case ModbusError.SLAVE_DEVICE_FAILURE: return "Device replied with 'slave device failure'"; case ModbusError.SLAVE_DEVICE_BUSY: return "Device replied with 'slave device busy'"; case ModbusError.NO_RESPONSE_FROM_TARGET: return "Device replied with 'no response from target'"; case ModbusError.UNKNOWN_SLAVE_ERROR: return "Device replied with an unknown error code"; case ModbusError.COMMUNICATION_ERROR: return "Communication error"; case ModbusError.INTERNAL_ERROR: return "Internal error"; case ModbusError.OK: return "OK"; default: return String.Format( "unknown error {0}", err ); } } } /// @cond DEV public abstract class Constants { // Function codes defined by the modbus protocol. protected enum Function { /** * This function code is used to read from 1 to 2000 contiguous * status of coils in a remote device. The Request PDU specifies * the starting address, i.e. the address of the first coil * specified, and the number of coils. In the PDU Coils are * addressed starting at zero. Therefore coils numbered 1-16 are * addressed as 0-15. * * The coils in the response message are packed as one coil per * bit of the data field. Status is indicated as 1= ON and 0= OFF. * The LSB of the first data byte contains the output addressed in * the query. The other coils follow toward the high order end of * this byte, and from low order to high order in subsequent bytes. * * If the returned output quantity is not a multiple of eight, the * remaining bits in the final data byte will be padded with zeros * (toward the high order end of the byte). The Byte Count field * specifies the quantity of complete bytes of data. * * Request: * Function code 1 Byte 0x01 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of coils 2 Byte 1 to 2000 (0x7D0) * * Reply: * Function code 1 Byte 0x01 * Byte count 1 Byte N * Coil status N Byte */ READ_COILS = 0x01, /** * This function code is used to read from 1 to 2000 contiguous * status of discrete inputs in a remote device. The Request PDU * specifies the starting address, i.e. the address of the first * input specified, and the number of inputs. In the PDU Discrete * Inputs a re addressed starting at zero. Therefore Discrete * inputs numbered 1-16 are addressed as 0-15. * * The discrete inputs in the response message are packed as one * input per bit of the data field. Status is indicated as * 1= ON; 0= OFF. The LSB of the first data byte contains the * input addressed in the query. The other inputs follow toward * the high order end of this byte, and from low order to high * order in subsequent bytes. * * If the returned input quantity is not a multiple of eight, * the remaining bits in the final d ata byte will be padded * with zeros (toward the high order end of the byte). The Byte * Count field specifies the quantity of complete bytes of data. * * Request: * Function code 1 Byte 0x02 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of inputs 2 Byte 1 to 2000 (0x7D0) * * Reply: * Function code 1 Byte 0x02 * Byte count 1 Byte N * Input status N Byte */ READ_DISCRETE_INPUTS = 0x02, /** * This function code is used to read the contents of a * contiguous block of holding registers in a remote device. * The Request PDU specifies the starting r egister address and * the number of registers. In the PDU Registers are addressed * starting at zero. Therefore registers numbered 1-16 are * addressed as 0-15. * * The register data in the response message are packed as two * bytes per register, with the binary contents right justified * within each byte. For each register, the first byte contains * the high order bits and the second contains the low order bits. * * Request: * Function code 1 Byte 0x03 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of registers 2 Byte 1 to 125 (0x7D) * * Reply: * Function code 1 Byte 0x03 * Byte count 1 Byte N * Register values N*2 Byte */ READ_HOLDING_REGISTERS = 0x03, /** * This function code is used to read from 1 to 125 contiguous * input registers in a remote device. The Request PDU specifies * the starting register address and the number of registers. In * the PDU Registers are addressed starting at zero. Therefore * input registers n umbered 1-16 are addressed as 0-15. * * The register data in the response message are packed as two * bytes per register, with the binary contents right justified * within each byte. For each register, the first byte contains * the high order bits and the second contains the low order bits. * * Request: * Function code 1 Byte 0x04 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of registers 2 Byte 1 to 125 (0x7D) * * Reply: * Function code 1 Byte 0x04 * Byte count 1 Byte N * Register values N*2 Byte */ READ_INPUT_REGISTERS = 0x04, /** * This function code is used to write a single output to either * ON or OFF in a remote device. The requested ON/OFF state is * specified by a constant in the request data field. A value of * FF 00 hex requests the output to be ON. A value of 00 00 * requests it to be OFF. All other values are illegal and will * not affect the output. * * The Request PDU specifies the address of the coil to be forced. * Coils are addressed starting at zero. Therefore coil numbered 1 * is addressed as 0. The requested ON/OFF state is specified by a * constant in the Coil Value field. A value of 0XFF00 requests the * coil to be ON. A value of 0X0000 requests the coil to be off. * All other values are illegal and will not affect the coil. * * The normal response is an echo of the request, returned after * the coil state has been written. * * Request: * Function code 1 Byte 0x05 * Output address 2 Byte 0x0000 to 0xFFFF * Output value 2 Byte 0x0000 or 0xFF00 * * Reply: * Function code 1 Byte 0x05 * Output address 2 Byte 0x0000 to 0xFFFF * Output value 2 Byte 0x0000 or 0xFF00 */ WRITE_SINGLE_COIL = 0x05, /** * This function code is used to write a single holding register * in a remote device. * * The Request PDU specifies the address of the register to be * written. Registers are addressed starting at zero. Therefore * register numbered 1 is addressed as 0. * * The normal response is an echo of the request, returned after * the register contents have been written. * * Request: * Function code 1 Byte 0x05 * Register address 2 Byte 0x0000 to 0xFFFF * Register Value 2 Byte 0x0000 to 0xFFFF * * Reply: * Function code 1 Byte 0x05 * Register address 2 Byte 0x0000 to 0xFFFF * Register Value 2 Byte 0x0000 to 0xFFFF */ WRITE_SINGLE_REGISTER = 0x06, /* * This function code is used to read the contents of eight * Exception Status outputs in a remote device. * * The function provides a simple method for accessing this * information, because the Exception Output references are * known (no output reference is needed in the function). * * The normal response contains the status of the eight Exception * Status outputs. The outputs are packed into one data byte, with * one bit per output. The statu s of the lowest output reference * is contained in the least significant bit of the byte. * * The contents of the eight Exception Status outputs are device * specific. * READ_EXCEPTION_STATUS = 0x07, */ /* * MODBUS function code 08 provides a series of tests for checking * the communication system between a client device and a server, * or for checking various internal error conditions within a * server. * * The function uses a two–byte sub-function code field in the * query to define the type of test to be performed. The server * echoes both the function code and sub -function code in a normal * response. Some of the diagnostics cause data to be returned from * the remote device in the data field of a normal response. * * In general, issuing a diagnostic function to a remote device * does not affect the running of the user program in the remote * device. User logic, like discrete and registers, is not * accessed by the diagnostics. Certain functions can optionally * reset error counters in the remote device. * * A server device can, however, be forced into 'Listen Only Mode' * in which it will monitor the messages on the communications * system but not respond to them. This can affect the outcome * of your application program if it depends upon any fu rther * exchange of data with the remote device. Generally, the mode * is forced to remove a malfunctioning remote device from the * communications system. * * The following diagnostic functions are dedicated to serial * line devices. * * The normal response to the Return Query Data request is to * loopback the same data. The function code and sub-function * codes are also echoed. * DIAGNOSTICS = 0x08, */ /* * This function code is used to get a status word and an event * count from the remote device's communication event counter. * * By fetching the current count before and after a series of * messages, a client can determine whether the messages were * handled normally by the remote device. The device’s event * counter is incremented once for each successful message * completion. It is not incremented for exception responses, * poll commands, or fetch event counter commands. * * The event counter can be reset by means of the Diagnostics * function (code 08), with a sub - function of Restart * Communications Option (code 00 01) or Clear Counters and * Diagnostic Register (code 00 0A). * * The normal response contains a two-byte status word, and a * two-byte event count. The status word will be all ones * (FF FF hex) if a previously–issued program command is still * being processed by the remote device (a busy condition * exists). Otherwise, the status word will be all zeros. * GET_COMM_EVENT_COUNTER = 0x0B, */ /* * This function code is used to get a status word, event count, * message count, and a field of event bytes from the remote * device. * * The status word and event counts are identical to that * returned by the Get Communications Event Counter function * (11, 0B hex). * * The message counter contains the quantity of messages * processed by the remote device since its last restart, * clear counters operation, or power-up. This count is * identical to that returned by the Diagnostic function * (code 08), sub-function Return Bus Message Count * (code 11, 0B hex). * * The event bytes field contains 0-64 bytes, with each byte * corresponding to the status of one MODBUS send or receive * operation for the remote device. The rem ote device enters * the events into the field in chronological order. Byte 0 is * the most recent event. Each new byte flushes the oldest byte * from the field. * * The normal response contains a two–byte status word field, * a two-byte event count field, a two–byte message count field, * and a field containing 0-64 bytes of events. A byte count * field defines the total length of the data in these four * fields. * GET_COMM_EVENT_LOG = 0x0C, */ /** * This function code is used to force each coil in a sequence * of coils to either ON or OFF in a remote device. The Request * PDU specifies the coil references to be forced. Coils are * addressed starting at zero. Therefore coil numbered 1 is * addressed as 0. * * The requested ON/OFF states are specified by contents of the * request data field. A logical ' 1' in a bit position of the * field requests the corresponding output to be ON. A logical * '0' requests it to be OFF. * * The normal response returns the function code, starting * address, and quantity of coils forced. * * Request: * Function code 1 Byte 0x0F * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of outputs 2 Byte 0x0001 to 0x07B0 * Byte count 1 Byte N * Output values N*1 Byte * * Reply: * Function code 1 Byte 0x0F * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of outputs 2 Byte 0x0000 to 0x07B0 */ WRITE_MULTIPLE_COILS = 0x0F, /** * This function code is used to write a block of contiguous * registers (1 to 123 registers) in a remote device. * * The requested written values are specified in the request * data field. Data is packed as two bytes per register. * * The normal response returns the function code, starting * address, and quantity of registers written. * * Request: * Function code 1 Byte 0x10 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of registers 2 Byte 0x0001 to 0x007B * Byte count 1 Byte 2*N * Register values N*2 Byte * * Reply: * Function code 1 Byte 0x10 * Starting address 2 Byte 0x0000 to 0xFFFF * Quantity of outputs 2 Byte 0x0000 to 0x07B0 */ WRITE_MULTIPLE_REGISTERS = 0x10, /* * This function code is used to read the description of the * type, the current status, and other information specific to * a remote device. * * The format of a normal response is shown in the following * example. The data contents are specific to each type of * device. * REPORT_SERVER_ID = 0x11, */ /* * This function code is used to perform a file record read. All * Request Data Lengths are provided in terms of number of bytes * and all Record Lengths are provided in terms of registers. * * A file is an organization of records. Each file contains 10000 * records, addressed 0000 to 9999 decimal or 0X0000 to 0X270F. * For example, record 12 is addressed as 12. * * The function can read multiple groups of references. The groups * can be separating (non - contiguous), but the references within * each group must be sequential. * * Each group is defined in a separate ‘sub-request’ field that contains 7 bytes: * - The reference type: 1 byte (must be specified as 6) * - The File number: 2 bytes * - The starting record number within the file: 2 bytes * - The length of the record to be read: 2 bytes. * * The quantity of registers to be read, combined with all other * fields in the expected response, must not exceed the allowable * length of the MODBUS PDU : 253 bytes. * * The normal response is a series of 'sub-responses', one for each * 'sub-request'. The byte count field is the total combined count * of bytes in all 'sub-responses'. In addition, each * 'sub-response' contains a field that shows its own byte count. * READ_FILE_RECORD = 0x14, */ /* * This function code is used to perform a file record write. All * Request Data Lengths are provided in terms of number of bytes * and all Record Lengths are provided in terms of the number of * 16-bit words. * * A file is an organization of records. Each file contains 10000 * records, addressed 0000 to 9999 decimal or 0x0000 to 0x270F. For * example, record 12 is addressed as 12. * * The function can write multiple groups of references. The groups * can be separate, i.e. non-contiguous, but the references within * each group must be sequential. * * Each group is defined in a separate 'sub-request' field that * contains 7 bytes plus the data: * * The reference type: 1 byte (must be specified as 6) * - The file number: 2 bytes * - The starting record number within the file: 2 bytes * - The length of the record to be written: 2 bytes * - The data to be written: 2 bytes per register. * * The quantity of registers to be written, combined with all * other fields in the request, must not exceed the allowable * length of the MODBUS PDU : 253bytes. * * The normal response is an echo of the request. * WRITE_FILE_RECORD = 0x15, */ /** * This function code is used to modify the contents of a * specified holding register using a combination of an AND * mask, an OR mask, and the register's current contents. The * function can be used to set or clear individual bits in the * register. * * The request specifies the holding register to be written, the * data to be used as the AND mask, and the data to be used as * the OR mask. Registers are addressed starting at zero. * Therefore registers 1-16 are addressed as 0-15. * * The function's algorithm is: * * Result = (Current Contents AND And_Mask) OR (Or_Mask AND (NOT And_Mask)) * R = (C & A) | (O & ~A) * * For example: * Hex Binary * Current Contents = 12 0001 0010 * And_Mask = F2 1111 0010 * Or_Mask = 25 0010 0101 * (NOT And_Mask) = 0D 0000 1101 * Result = 17 0001 0111 * * Note: * - If the Or_Mask value is zero, the result is simply the logical * ANDing of the current contents and And_Mask. If the And_Mask * value is zero, the result is equal to the Or_Mask value. * - The contents of the register can be read with the Read Holding * Registers function (function code 03). They could, however, be * changed subsequently as the controller scans its user logic * program. * * The normal response is an echo of the request. The response is * returned after the register has been written. * * Request: * Function code 1 Byte 0x16 * Register address 2 Byte 0x0000 to 0xFFFF * AND mask 2 Byte 0x0000 to 0xFFFF * OR mask 2 Byte 0x0000 to 0xFFFF * * Reply: * Function code 1 Byte 0x16 * Register address 2 Byte 0x0000 to 0xFFFF * AND mask 2 Byte 0x0000 to 0xFFFF * OR mask 2 Byte 0x0000 to 0xFFFF */ MASK_WRITE_REGISTER = 0x16, /** * This function code performs a combination of one read operation * and one write operation in a single MODBUS transaction. The write * operation is performed before the read. * * Holding registers are addressed starting at zero. Therefore * holding registers 1 -16 are addressed in the PDU as 0-15. * * The request specifies the starting address and number of holding * registers to be read as well as the starting address, number of * holding registers, and the data to be written. The byte count * specifies the number of bytes to follow in the write data field. * * The normal response contains the data from the group of registers * that were read. The byte count field specifies the quantity of * bytes to follow in the read data field. * * Request: * Function code 1 Byte 0x17 * Read starting address 2 Byte 0x0000 to 0xFFFF * Quantity to read 2 Byte 0x0001 to 0x007D * Write starting address 2 Byte 0x0000 to 0xFFFF * Quantity to write 2 Byte 0x0001 to 0x0079 * Write byte count 1 Byte N*2 (N = quantity to write) * Write register values N*2 Byte * * Reply: * Function code 1 Byte 0x17 * Read byte count 1 Byte N*2 (N = quantity to read) * Read register values N*2 Byte */ READ_WRITE_MULTIPLE_REGISTERS = 0x17, /* * This function code allows to read the contents of a * First-In-First-Out (FIFO) queue of register in a remote device. * The function returns a count of the registers in the queue, * followed by the queued data. Up to 32 registers can be read: * the count, plus up to 31 queued data registers. * * The queue count register is returned first, followed by the * queued data registers. * * The function reads the queue contents, but does not clear them. * * In a normal response, the byte count shows the quantity of bytes * to follow, including the queue count bytes and value register * bytes (but not including the error check field). * * The queue count is the quantity of data registers in the queue * (not including the count register). * If the queue count exceeds 31, an exception response is returned * with an error code of 03 (Illegal Data Value). * READ_FIFO_QUEUE = 0x18, */ /* * Informative Note: The user is asked to refer to Annex A * (Informative) MODBUS RESERVED FUNCTION CODES, SUBCODES AND * MEI TYPES. * * Function Code 43 and its MEI Type 14 for Device Identification * is one of two Encapsulated Interface Transport currently * available in this Specification. The following function codes * and MEI Types shall not be part of this published Specification * and these function codes and MEI Types are specifically * reserved: 43/0-12 and 43/15-255. * * The MODBUS Encapsulated Interface (MEI)Transport is a mechanism * for tunneling service requests and method invocations, as well * as their returns, inside MODBUS PDUs . * * The primary feature of the MEI Transport is the encapsulation * of method invocations or service requests that are part of a * defined interface as well as method invocation returns or * service responses. * ENCAPSULATED_INTERFACE_TRANSPORT = 0x2B */ } // Error codes defined by the modbus protocol. protected enum ErrorCode { OK = 0x00, ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDRESS = 0x02, ILLEGAL_DATA_VALUE = 0x03, SERVER_DEVICE_FAILURE = 0x04, ACKNOWLEDGE = 0x05, SERVER_DEVICE_BUSY = 0x06, MEMORY_PARITY_ERROR = 0x07, GATEWAY_PATH_UNAVAILABLE = 0x0A, GATEWAY_TARGET_DEV_NO_RESPONSE = 0x0B } protected const int DEFAULT_TCP_PORT = 502; protected const int ERROR_REPLY_SIZE = 2; protected const int OFF_FUNCTION = 0; protected const int OFF_ERROR_CODE = 1; protected const byte FUNC_ERROR_FLAG = 0x80; protected const int MIN_QUERY_SIZE = 1; } /** * A modbus frame is a frame that is added around the message before sending. */ public abstract class Frame : Constants { /// Size of the header in the frame. public readonly int header_size; /// The sum of the header and the footer of the frame. public readonly int overhead; /// True if modbus master, false for slave, affects the recv functions. public readonly bool master_mode; protected byte[] send_buf; protected int send_cnt; protected byte[] recv_buf; protected int recv_cnt; protected int recv_exp; // private int recv_skipped; /** * Creates a new frame. * * @param _header_size Size of the header of the frame. * @param _overhead Sum of the sizes of the header and the footer. */ protected Frame( int _header_size, int _overhead, bool _master_mode ) { header_size = _header_size; overhead = _overhead; master_mode = _master_mode; send_buf = new byte[1024]; recv_buf = new byte[1024]; } // static functions accessing data in the specified buffer /** * Get the byte at the specified offset from the given buffer. */ public static byte frame_get_byte( byte[] buffer, int offset ) { return buffer[offset]; } /** * Set the byte at the specified offset in the given buffer. */ public static void frame_set_byte( byte[] buffer, int offset, byte data ) { buffer[offset] = data; } /** * Get the ushort at the specified offset from the given buffer. */ public static ushort frame_get_ushort( byte[] buffer, int offset ) { int n = (buffer[offset] << 8) | buffer[offset+1]; return (ushort)n; } /** * Set the ushort at the specified offset in the given buffer. */ public static void frame_set_ushort( byte[] buffer, int offset, ushort data ) { buffer[offset] = (byte)(data >> 8); buffer[offset+1] = (byte)data; } // protected byte functions accessing the whole frame /** * Get the byte at the specified offset from the send buffer. */ protected byte frame_get_send_byte( int offset ) { return frame_get_byte( send_buf, offset ); } /** * Set the byte at the specified offset in the send buffer. */ protected void frame_set_send_byte( int offset, byte data ) { frame_set_byte( send_buf, offset, data ); } /** * Get the byte at the specified offset from the recv buffer. */ protected byte frame_get_recv_byte( int offset ) { return frame_get_byte( recv_buf, offset ); } /** * Set the byte at the specified offset in the recv buffer. */ protected void frame_set_recv_byte( int offset, byte data ) { frame_set_byte( recv_buf, offset, data ); } // protected ushort functions accessing the whole frame /** * Get the ushort at the specified offset from the send buffer. */ protected ushort frame_get_send_ushort( int offset ) { return frame_get_ushort( send_buf, offset ); } /** * Set the ushort at the specified offset in the send buffer. */ protected void frame_set_send_ushort( int offset, ushort data ) { frame_set_ushort( send_buf, offset, data ); } /** * Get the ushort at the specified offset from the recv buffer. */ protected ushort frame_get_recv_ushort( int offset ) { return frame_get_ushort( recv_buf, offset ); } /** * Set the ushort at the specified offset in the recv buffer. */ protected void frame_set_recv_ushort( int offset, ushort data ) { frame_set_ushort( recv_buf, offset, data ); } // public byte functions accessing the frame content /** * Get the byte at the specified offset from the send buffer. */ public byte get_send_byte( int offset ) { return frame_get_byte( send_buf, offset + header_size ); } /** * Set the byte at the specified offset in the send buffer. */ public void set_send_byte( int offset, byte data ) { frame_set_byte( send_buf, offset + header_size, data ); } /** * Get the byte at the specified offset from the recv buffer. */ public byte get_recv_byte( int offset ) { return frame_get_byte( recv_buf, offset + header_size ); } /** * Set the byte at the specified offset in the recv buffer. public void set_recv_byte( int offset, byte data ) { frame_set_byte( recv_buf, offset + header_size, data ); } */ // public ushort functions accessing the frame content /** * Get the ushort at the specified offset from the send buffer. */ public ushort get_send_ushort( int offset ) { return frame_get_ushort( send_buf, offset + header_size ); } /** * Set the ushort at the specified offset in the send buffer. */ public void set_send_ushort( int offset, ushort data ) { frame_set_ushort( send_buf, offset + header_size, data ); } /** * Get the ushort at the specified offset from the recv buffer. */ public ushort get_recv_ushort( int offset ) { return frame_get_ushort( recv_buf, offset + header_size ); } /** * Set the ushort at the specified offset in the recv buffer. public void set_recv_ushort( int offset, ushort data ) { frame_set_ushort( recv_buf, offset + header_size, data ); } */ // functions for accessing the internal variables public byte[] get_send_buf() { return send_buf; } public byte[] get_recv_buf() { return recv_buf; } public int get_send_cnt() { return send_cnt; } public int get_recv_cnt() { return recv_cnt; } public int get_recv_exp() { return recv_exp; } public int get_recv_pdu_size() { return recv_cnt - overhead; } // abstract functions /** * Returns the (slave) unit ID from the message in the send buffer. */ public abstract byte get_send_unit_id(); /** * Returns the (slave) unit ID from the message in the recv buffer. */ public abstract byte get_recv_unit_id(); /** * Initialize the frame around the message. */ public abstract void initialize( int pdu_size, byte slave_id ); /** * Finish the frame around the message before sending. */ public abstract void finish( int pdu_size ); /** * Check the received frame. */ public abstract bool check( int pdu_size ); public abstract void clear_recv_buf( int pdu_size ); public abstract int get_recv_req(); public bool received_data( byte[] data, Logger logger ) { int n = data.Length; if( recv_cnt < recv_buf.Length && (recv_cnt + n) <= recv_buf.Length ) { Buffer.BlockCopy( data, 0, recv_buf, recv_cnt, n ); } // return received_data( n ); bool rc = received_data( n ); if( rc ) { logger.log( LogLevel.DEBUG2, "Frame.received_data: done." ); } return rc; } public abstract bool received_data( int n ); public String toString( bool send ) { byte[] buf; int len; String msg; if( send ) { buf = send_buf; len = send_cnt; msg = string.Format( "{0} bytes:", len ); } else { buf = recv_buf; len = recv_cnt; msg = string.Format( "{0}/{1} bytes:", len, recv_exp ); } int footer_pos = recv_exp - (overhead - header_size); for( int i=0; i 0 && ( i == header_size || i == footer_pos || i == recv_exp ) ) { msg += " :"; } msg += string.Format( " {0:X2}", buf[i] ); } return msg; } public void print_msg( string msg, bool send, Logger logger, LogLevel level=LogLevel.DEBUG3 ) { if( logger.is_on( level ) ) { logger.log( level, msg + " " + toString( send ) ); } } } /** * ADU frame used in modbus RTU. */ public class FrameAdu : Frame { // Offset of the unit ID field. public const int OFF_UNIT_ID = 0; // Size of the header. public const int HEADER_SIZE = 1; // Sum of the sizes of the header and the footer. public const int OVERHEAD = 3; /** * */ public FrameAdu( bool _master_mode ) : base( HEADER_SIZE, OVERHEAD, _master_mode ) { } /** * Returns the (slave) unit ID from the message in the send buffer. */ public override byte get_send_unit_id() { return frame_get_byte( send_buf, OFF_UNIT_ID ); } /** * Returns the (slave) unit ID from the message in the recv buffer. */ public override byte get_recv_unit_id() { return frame_get_byte( recv_buf, OFF_UNIT_ID ); } /** * Initializes the message frame in the send buffer. */ public override void initialize( int pdu_size, byte slave_id ) { frame_set_byte( send_buf, OFF_UNIT_ID, slave_id ); send_cnt = OVERHEAD + pdu_size; } /** * This function adds the CRC to the message frame in the send buffer. */ public override void finish( int pdu_size ) { /* int off = header_size - pdu_size; ushort crc = calc_crc( message, off ); */ int off = send_cnt - 2; ushort crc = calc_crc( send_buf, off ); frame_set_ushort( send_buf, off, crc ); } /** * This function checks message frame in the receive buffer. */ public override bool check( int pdu_size ) { // int off = header_size - pdu_size; int off = recv_cnt - 2; ushort crc = calc_crc( recv_buf, off ); return frame_get_ushort( recv_buf, off ) == crc; } public override void clear_recv_buf( int pdu_size ) { if( master_mode ) { recv_exp = OVERHEAD + pdu_size; } else { // minimum request size recv_exp = OVERHEAD + 1; } recv_cnt = 0; } public override int get_recv_req() { return recv_exp - recv_cnt; } public override bool received_data( int n ) { recv_cnt += n; if( recv_cnt == recv_exp ) { byte func = get_recv_byte( OFF_FUNCTION ); if( recv_cnt == (OVERHEAD + MIN_QUERY_SIZE) ) { switch( func ) { case (byte)Function.READ_COILS: recv_exp = OVERHEAD + 5; break; case (byte)Function.READ_DISCRETE_INPUTS: recv_exp = OVERHEAD + 5; break; case (byte)Function.READ_HOLDING_REGISTERS: recv_exp = OVERHEAD + 5; break; case (byte)Function.READ_INPUT_REGISTERS: recv_exp = OVERHEAD + 5; break; case (byte)Function.WRITE_SINGLE_COIL: recv_exp = OVERHEAD + 5; break; case (byte)Function.WRITE_SINGLE_REGISTER: recv_exp = OVERHEAD + 5; break; /* case (byte)Function.READ_EXCEPTION_STATUS: recv_exp = OVERHEAD + 1; break; case (byte)Function.DIAGNOSTICS: // func (1) + subfunc (2) + data (n*2) recv_exp = OVERHEAD + 0; break; case (byte)Function.GET_COMM_EVENT_COUNTER: recv_exp = OVERHEAD + 1; break; case (byte)Function.GET_COMM_EVENT_LOG: recv_exp = OVERHEAD + 1; break; */ case (byte)Function.WRITE_MULTIPLE_COILS: // overhead+6 + N bytes recv_exp = OVERHEAD + 6; break; case (byte)Function.WRITE_MULTIPLE_REGISTERS: // overhead+6 + N * 2 bytes recv_exp = OVERHEAD + 6; break; /* case (byte)Function.REPORT_SERVER_ID: recv_exp = OVERHEAD + 1; break; case (byte)Function.READ_FILE_RECORD: // overhead+9 + ??? recv_exp = OVERHEAD + 0; break; case (byte)Function.WRITE_FILE_RECORD: // overhead+9 + ??? recv_exp = OVERHEAD + 0; break; */ case (byte)Function.MASK_WRITE_REGISTER: recv_exp = OVERHEAD + 7; break; case (byte)Function.READ_WRITE_MULTIPLE_REGISTERS: // overhead+10 + N * 2 bytes recv_exp = OVERHEAD + 10; break; /* case (byte)Function.READ_FIFO_QUEUE: recv_exp = OVERHEAD + 3; break; case (byte)Function.ENCAPSULATED_INTERFACE_TRANSPORT: // overhead+2 + N bytes recv_exp = OVERHEAD + 0; break; */ default: // error ++++++ break; } } else if( func == (byte)Function.WRITE_MULTIPLE_COILS ) { recv_exp += (ushort)get_recv_byte( 5 ); } else if( func == (byte)Function.READ_WRITE_MULTIPLE_REGISTERS ) { recv_exp += 2 * (ushort)get_recv_byte( 9 ); } else if( func == (byte)Function.WRITE_MULTIPLE_REGISTERS ) { // nothing to do here, reply length is constant } else { // ++++++ error!?! } } else if( recv_cnt == (OVERHEAD + 2) && (get_recv_byte( OFF_FUNCTION ) & FUNC_ERROR_FLAG) != 0 ) { // error reply recv_exp = (OVERHEAD + 2); } return recv_cnt >= recv_exp; } /** * Calculate the CRC of the message. */ private static ushort calc_crc( byte[] buffer, int length ) { byte hi = 0xFF; byte lo = 0xFF; byte i; for( int j=0; j recv_buf.Length ) { return recv_buf.Length - recv_cnt; } else { return recv_exp - recv_cnt; } */ int rcnt = recv_cnt - recv_skipped; if( rcnt > recv_buf.Length ) { return recv_buf.Length - rcnt; } else { return recv_exp - recv_cnt; } } public override bool received_data( int n ) { recv_cnt += n; if( recv_cnt == HEADER_SIZE ) { recv_exp += frame_get_recv_ushort( OFF_LENGTH_HI ) - 1; if( recv_exp > recv_buf.Length ) { // oversized message, mark it as invalid frame_set_recv_ushort( OFF_PROTO_ID_HI, PROTO_INVALID ); } } int blen = recv_buf.Length + recv_skipped; if( recv_exp > blen && recv_cnt == blen ) { // throw away some of the data in the buffer /* n = recv_buf.Length - (HEADER_SIZE + 2); recv_exp -= n; recv_skipped += n; */ recv_skipped += recv_buf.Length - (HEADER_SIZE + 2); } return recv_cnt == recv_exp; } } public interface MessageLog { void logModbusMessage( bool sending, String message ); void logModbusError( ModbusError error ); void logModbusInfo( String text ); } public abstract class Base : Constants { protected String name; protected Frame frame; protected Logger logger; protected MessageLog? messageLog; public Base( String _name, Frame _frame, Logger _logger ) { name = _name; frame = _frame; logger = _logger; messageLog = null; } public void setMessageLog( MessageLog? msglog ) { messageLog = msglog; } } /// @endcond public abstract class Master : Base, Calls { public static Master create( String connspec, Logger? logger=null ) { String spec = connspec.ToLower(); if( spec.StartsWith( "tcp-" ) ) { String host; ushort port; if( !parse_tcp_spec( connspec, out host, out port ) ) { throw new ArgumentException( "Invalid modbus TCP connection specification.", "connspec" ); } return new MasterTcp( host, port, logger ); } else if( spec.StartsWith( "tcpadu-" ) ) { String host; ushort port; if( !parse_tcp_spec( connspec, out host, out port ) ) { throw new ArgumentException( "Invalid modbus TCPADU connection specification.", "connspec" ); } return new MasterTcpAdu( host, port, logger ); } else { return new MasterRtu( connspec, logger ); } } private static bool parse_tcp_spec( String connspec, out String host, out ushort port ) { String[] parts = connspec.Split( new char[]{ '-' } ); if( parts.Length != 3 ) { host = ""; port = 0; return false; } host = parts[1]; return ushort.TryParse( parts[2], out port ); } public Master( String _name, Frame _frame, Logger _logger ) : base( _name, _frame, _logger ) { } public ModbusError read_holding_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { ModbusError me; me = check_args_for_multiple_registers( "read_holding_registers", slave_id, addr, data.Length, ref count, offset ); if( me != ModbusError.OK ) { return me; } me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 5; int recv_pdu_size = 2 + count * 2; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.READ_HOLDING_REGISTERS ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, (ushort)count ); frame.finish( send_pdu_size ); me = communicate( send_pdu_size, recv_pdu_size ); // +++ check length field? if( me == ModbusError.OK ) { for( int i=2; count>0; count--,i+=2,offset++ ) { data[offset] = frame.get_recv_ushort( i ); } } return me; } public ModbusError read_input_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { ModbusError me; me = check_args_for_multiple_registers( "read_input_registers", slave_id, addr, data.Length, ref count, offset ); if( me != ModbusError.OK ) { return me; } me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 5; int recv_pdu_size = 2 + count * 2; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.READ_INPUT_REGISTERS ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, (ushort)count ); frame.finish( send_pdu_size ); me = communicate( send_pdu_size, recv_pdu_size ); // +++ check length field? if( me == ModbusError.OK ) { for( int i=2; count>0; count--,i+=2,offset++ ) { data[offset] = frame.get_recv_ushort( i ); } } return me; } public ModbusError write_single_register( byte slave_id, ushort addr, ushort data ) { ModbusError me; me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 5; int recv_pdu_size = 5; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.WRITE_SINGLE_REGISTER ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, data ); frame.finish( send_pdu_size ); me = communicate( send_pdu_size, recv_pdu_size ); return me; } public ModbusError write_multiple_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { ModbusError me; me = check_args_for_multiple_registers( "write_multiple_registers", slave_id, addr, data.Length, ref count, offset ); if( me != ModbusError.OK ) { return me; } me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 6 + count * 2; int recv_pdu_size = 5; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.WRITE_MULTIPLE_REGISTERS ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, (ushort)count ); frame.set_send_byte( 5, (byte)(2 * count) ); for( int i=0; i= data_length ) { logger.log( LogLevel.ERROR, "error in {0}({1},{2},{3}) too few regs", func_name, name, slave_id, addr ); return ModbusError.INVALID_OFFSET; } if( count < 0 ) { count = data_length - offset; } if( count < 1 ) { logger.log( LogLevel.ERROR, "error in {0}({1},{2},{3}) too few regs", func_name, name, slave_id, addr ); return ModbusError.TOO_FEW_REGISTERS; } if( (count + offset) > data_length ) { logger.log( LogLevel.ERROR, "error in {0}({1},{2},{3}) count ({4}+{5}={6}) > length ({7})", func_name, name, slave_id, addr, count, offset, count + offset, data_length ); return ModbusError.INVALID_OFFSET; } if( count > 250 ) { logger.log( LogLevel.ERROR, "error in {0}({1},{2},{3}) too many regs", func_name, name, slave_id, addr ); return ModbusError.TOO_MANY_REGISTERS; } int end = addr + count - 1; if( end > 65535 ) { logger.log( LogLevel.ERROR, "error in {0}({1},{2},{3}) last reg too high", func_name, name, slave_id, addr ); return ModbusError.LAST_REG_NR_TOO_HIGH; } return ModbusError.OK; } public ModbusError read_discrete_inputs( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { ModbusError me; //logger.log( LogLevel.DEBUG3, "read_discrete_inputs slave={0} addr={1} count={2} offset={3}", slave_id, addr, count, offset ); me = check_args_for_multiple_registers( "read_discrete_inputs", slave_id, addr, data.Length*16, ref count, offset ); if( me != ModbusError.OK ) { return me; } me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 5; int recv_pdu_size = 2 + (count + 7) / 8; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.READ_DISCRETE_INPUTS ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, (ushort)count ); frame.finish( send_pdu_size ); me = communicate( send_pdu_size, recv_pdu_size ); // +++ check length field? if( me == ModbusError.OK ) { copy_bit_data( 2, data, offset, count ); } return me; } public ModbusError read_coils( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { ModbusError me; //logger.log( LogLevel.DEBUG3, "read_coils slave={0} addr={1} count={2} offset={3}", slave_id, addr, count, offset ); me = check_args_for_multiple_registers( "read_coils", slave_id, addr, data.Length*16, ref count, offset ); if( me != ModbusError.OK ) { return me; } me = open(); if( me != ModbusError.OK ) { return me; } // initialize message int send_pdu_size = 5; int recv_pdu_size = 2 + (count + 7) / 8; frame.initialize( send_pdu_size, slave_id ); frame.set_send_byte( OFF_FUNCTION, (byte)Function.READ_COILS ); frame.set_send_ushort( 1, addr ); frame.set_send_ushort( 3, (ushort)count ); frame.finish( send_pdu_size ); me = communicate( send_pdu_size, recv_pdu_size ); // +++ check length field? if( me == ModbusError.OK ) { copy_bit_data( 2, data, offset, count ); } return me; } private void copy_bit_data( int i, ushort[] data, int offset, int count ) { //logger.log( LogLevel.DEBUG3, "copy_bit_data i={0} o={1} c={2}", i, offset, count ); while( count >= 16 ) { ushort bl = frame.get_recv_byte( i ); ushort bh = frame.get_recv_byte( i+1 ); //logger.log( LogLevel.DEBUG3, "bl={0:X02} bh={1:X02}", bl, bh ); bl &= 0xFF; bh <<= 8; data[offset] = (ushort)(bl | bh); //logger.log( LogLevel.DEBUG3, "data[{0}] = 0x{1:X04}", offset, data[offset] ); count -= 16; i += 2; offset++; } if( count > 0 ) { int val = frame.get_recv_byte( i ) & 0xFF; //logger.log( LogLevel.DEBUG3, "bl={0:X02}", val ); val &= (1 << count) - 1; data[offset] = (ushort)val; //logger.log( LogLevel.DEBUG3, "data[{0}] = 0x{1:X04}", offset, data[offset] ); } } public ModbusError write_single_coil( byte slave_id, ushort addr, bool value ) { return ModbusError.INTERNAL_ERROR; } public ModbusError write_multiple_coils( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ) { return ModbusError.INTERNAL_ERROR; } /** * Main communication function. */ private ModbusError communicate( int send_pdu_size, int recv_pdu_size ) { frame.clear_recv_buf( recv_pdu_size ); frame.print_msg( "Send", true, logger ); if( messageLog != null ) { messageLog.logModbusMessage( true, frame.toString( true ) ); } ModbusError me = communicate_impl(); if( frame.get_recv_cnt() > 0 ) { frame.print_msg( "Recv", false, logger ); if( messageLog != null ) { messageLog.logModbusMessage( false, frame.toString( false ) ); } } if( me != ModbusError.OK ) { // communicate_impl() returned an error messageLog?.logModbusError( me ); return me; } // int received_pdu_size = recv_cnt - frame.overhead; int received_pdu_size = frame.get_recv_pdu_size(); byte sfunc = frame.get_send_byte( OFF_FUNCTION ); byte rfunc = frame.get_recv_byte( OFF_FUNCTION ); if( received_pdu_size < ERROR_REPLY_SIZE ) { // shorter than an error message me = ModbusError.RECV_SHORT_REPLY; } else if( frame.get_recv_unit_id() != frame.get_send_unit_id() ) { // received message from different slave me = ModbusError.UNIT_ID_MISMATCH; } else if( !frame.check( recv_pdu_size ) ) { // CRC check failed me = ModbusError.CRC_MISMATCH; } else if( rfunc == sfunc ) { if( received_pdu_size == recv_pdu_size ) { // got a valid message me = ModbusError.OK; } else { // didn't receive enough bytes me = ModbusError.RECV_SHORT_REPLY; } } else if( (rfunc ^ FUNC_ERROR_FLAG) == sfunc ) { // got an error reply code if( received_pdu_size == ERROR_REPLY_SIZE ) { // got a valid error reply switch( frame.get_recv_byte( 1 ) ) { case (byte)ErrorCode.ILLEGAL_FUNCTION: me = ModbusError.ILLEGAL_FUNCTION; break; case (byte)ErrorCode.ILLEGAL_DATA_ADDRESS: me = ModbusError.ILLEGAL_DATA_ADDRESS; break; case (byte)ErrorCode.ILLEGAL_DATA_VALUE: me = ModbusError.ILLEGAL_DATA_VALUE; break; case (byte)ErrorCode.SERVER_DEVICE_FAILURE: me = ModbusError.SLAVE_DEVICE_FAILURE; break; case (byte)ErrorCode.SERVER_DEVICE_BUSY: me = ModbusError.SLAVE_DEVICE_BUSY; break; /* case (byte)ErrorCode.ACKNOWLEDGE: me = ModbusError.; break; case (byte)ErrorCode.MEMORY_PARITY_ERROR: me = ModbusError.; break; case (byte)ErrorCode.GATEWAY_PATH_UNAVAILABLE: me = ModbusError.; break; */ case (byte)ErrorCode.GATEWAY_TARGET_DEV_NO_RESPONSE: me = ModbusError.NO_RESPONSE_FROM_TARGET; break; default: me = ModbusError.UNKNOWN_SLAVE_ERROR; break; } } else { me = ModbusError.COMMUNICATION_ERROR; } } else { me = ModbusError.FUNCTION_MISMATCH; } communicate_result( me ); if( me != ModbusError.OK ) { // error processing the reply or error reply messageLog?.logModbusError( me ); } return me; } public abstract ModbusError open(); public abstract void close(); /** * Implementation of the device dependend communication. */ protected abstract ModbusError communicate_impl(); /** * Called at the end of communicate() to report the end result. */ protected abstract void communicate_result( ModbusError me ); public abstract void set_timeout_ms( ushort new_timeout_ms ); } public class MasterTcp : Master { private enum State { IDLE = 0, CONNECTING, QUERYING, GOT_REPLY, // GOT_ERROR, CLOSING } private string host; private ushort port; private int ip_index; // private IPHostEntry host_info; private IPAddress[] ip_addrs; private Socket? socket; private bool connected; ManualResetEvent connect_event; private Semaphore sem; private State state; // +++++ does this belong here? private int timeout; private int ok_delay; public MasterTcp( string hostname, Frame _frame, Logger? _logger=null ) : this( hostname, DEFAULT_TCP_PORT, _logger ) { } public MasterTcp( string hostname, ushort tcp_port, Logger? _logger=null ) : base( String.Format( "M:{0}:{1}", hostname, tcp_port ), new FrameMbap( true ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.MasterTcp" ) ) { host = hostname; port = tcp_port; ip_index = -1; connect_event = new ManualResetEvent(false); sem = new Semaphore( 0, 1 ); state = State.IDLE; // +++++ does this belong here? timeout = 350; ok_delay = 10; ip_addrs = get_ip_addrs( host ); } protected MasterTcp( string hostname, ushort tcp_port, String _name, Frame _frame, Logger _logger ) : base( _name, _frame, _logger ) { host = hostname; port = tcp_port; ip_index = -1; connect_event = new ManualResetEvent(false); sem = new Semaphore( 0, 1 ); state = State.IDLE; // +++++ does this belong here? timeout = 350; ok_delay = 10; ip_addrs = get_ip_addrs( host ); } static IPAddress[] get_ip_addrs( string host ) { try { IPAddress[] addrs = new IPAddress[1]; addrs[0] = IPAddress.Parse( host ); return addrs; } catch( Exception ) { IPHostEntry host_info = Dns.GetHostEntry( host ); return host_info.AddressList; } } public override ModbusError open() { if( connected ) { return ModbusError.OK; } string msg = ""; // +++++ timeouts??? try { // switch to next IP msg = "increase index"; ip_index++; if( ip_index < 0 || ip_index >= ip_addrs.Length ) { ip_index = 0; } msg = "choose endpoint"; name = String.Format( "{0}[{1}]:{2}", host, ip_addrs[ip_index], port ); msg = "create endpoint"; IPEndPoint endpoint = new IPEndPoint( ip_addrs[ip_index], port ); msg = "create socket"; socket = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); /* msg = "connect socket"; socket.Connect( endpoint ); */ msg = "connect socket: begin"; connect_event.Reset(); socket.BeginConnect( endpoint, new AsyncCallback(connect_callback), this ); msg = "connect socket: wait"; if( connect_event.WaitOne( 10*timeout ) ) { msg = "connect socket: conected"; connected = true; } else { msg = "connect socket: timeout"; Socket s = socket; socket = null; s.Close(); messageLog?.logModbusInfo( "TCP connect timeout." ); throw new Exception( "connect timeout" ); } } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} connect(): {1}", name, msg ); logger.log( LogLevel.WARNING, exc ); messageLog?.logModbusInfo( String.Format( "TCP connect exception ({0}).", exc.Message ) ); return ModbusError.TCP_CONNECT_ERROR; } return ModbusError.OK; } private static void connect_callback( IAsyncResult ar ) { MasterTcp? master = (MasterTcp?) ar.AsyncState; if( master != null ) { try { master.socket?.EndConnect( ar ); master.connect_event.Set(); } catch( Exception exc ) { master.logger.log( LogLevel.WARNING, exc ); } } } public override void close() { if( connected ) { logger.log( LogLevel.NOTICE, "modbus TCP client {0} close(): closing", name ); connected = false; try { socket?.Shutdown( SocketShutdown.Both ); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} close(): shutdown socket", name ); logger.log( LogLevel.WARNING, exc ); } try { socket?.Close(); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} close(): close socket", name ); logger.log( LogLevel.WARNING, exc ); } socket = null; } logger.log( LogLevel.NOTICE, "modbus TCP client {0} close(): closed", name ); } protected override ModbusError communicate_impl() { // clear possible event sem.WaitOne( 0 ); // send the send_buf state = State.QUERYING; send(); // wait for the signal that the communication has ended logger.log( LogLevel.DEBUG1, "modbus TCP client {0} sem.WaitOne( {1} )", name, timeout ); if( !sem.WaitOne( timeout ) ) { // timeout, close connection state = State.CLOSING; logger.log( LogLevel.NOTICE, "modbus TCP client {0} communicate(): timeout", name ); } ModbusError me; if( state == State.GOT_REPLY ) { me = ModbusError.OK; } else { logger.log( LogLevel.DEBUG1, "modbus TCP client {0} communicate(): got no reply", name ); close(); me = ModbusError.COMMUNICATION_ERROR; } state = State.IDLE; Thread.Sleep( ok_delay ); return me; } protected override void communicate_result( ModbusError me ) { // nothing to do here } private void printmsg( string t, byte[] m, int l ) { if( !logger.is_on( LogLevel.DEBUG1 ) ) { return; } String s = ""; for( int i=0; i {2}", name, timeout, new_timeout_ms ); timeout = new_timeout_ms; } } /** * A modbus master using RTU's ADU over TCP. * * When a simple ethernet to serial converter is connected to a modbus * device the protocol used over TCP is the same as the protocol on the * serial port. This master speaks modbus RTU/ADU over TCP. */ public class MasterTcpAdu : MasterTcp { public MasterTcpAdu( string hostname, ushort tcp_port, Logger? _logger=null ) : base( hostname, tcp_port, String.Format( "M:{0}:{1}-ADU", hostname, tcp_port ), new FrameAdu( true ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.MasterTcpAdu" ) ) { } } public class MasterRtu : Master { private SerPort serial; private bool receiver_on; private Semaphore sem; private bool had_error; private DateTime last_transfer; private int delay_normal_words; private int delay_error_words; private int delay_additional_ms; private int timeout_additional_ms; public MasterRtu( string port_spec, Logger? _logger=null ) : this( new SerPort( port_spec ), _logger ) { } public MasterRtu( string port_name, int baud_rate, Parity parity, int data_bits, StopBits stop_bits, Logger? _logger=null ) : this( new SerPort( port_name, baud_rate, parity, data_bits, stop_bits ), _logger ) { } private MasterRtu( SerPort _serial, Logger? _logger=null ) : base( "M:" + _serial.ToString(), new FrameAdu( true ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.MasterRtu" ) ) { serial = _serial; serial.set_read_callback( data_received ); receiver_on = false; sem = new Semaphore( 0, 1 ); // +++ calculate these depending on the baud rate delay_normal_words = 3; delay_error_words = 20; delay_additional_ms = 2; timeout_additional_ms = 500; } public override ModbusError open() { try { serial.open(); } catch( Exception exc ) { logger.log( LogLevel.ERROR, "MasterRtu.open {0}: open failed", serial.PortName ); logger.log( LogLevel.ERROR, exc ); return ModbusError.SERPORT_OPEN_ERROR; } return ModbusError.OK; } public override void close() { try { logger.log( LogLevel.NOTICE, "MasterRtu.close {0}: closing", serial.PortName ); serial.close(); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "MasterRtu.close {0}: caught exception", serial.PortName ); logger.log( LogLevel.WARNING, exc ); } logger.log( LogLevel.NOTICE, "MasterRtu.close {0}: closed", serial.PortName ); } protected override ModbusError communicate_impl() { int timeout; /* logger.log( LogLevel.DEBUG2, "modbus transceive tx {0} rx {1}", send_cnt, recv_pdu_size ); ushort crc = calc_crc( send_buf, send_cnt - 2 ); send_buf[send_cnt - 2] = (byte)(crc >> 8); send_buf[send_cnt - 1] = (byte)crc; */ // msg_slave_id = send_buf[RTU_ADU_UNIT_ID]; // msg_func= send_buf[RTU_ADU_HEADER_SIZE /* + FUNCTION */]; // recv_exp = recv_pdu_size + RTU_ADU_OVERHEAD; // recv_cnt = 0; receiver_on = true; sem.WaitOne( 0 ); Int64 ticks = (DateTime.UtcNow - last_transfer).Ticks; logger.log( LogLevel.DEBUG2, "modbus ticks {0}", ticks ); if( ticks < (1000*1000*10) ) { // additional bus delay timeout = calculate_timeout( had_error ? delay_error_words : delay_normal_words, delay_additional_ms ); int ticks_ms = (int)(ticks / 10000); if( ticks_ms < timeout && ticks_ms > 0 ) { timeout -= ticks_ms; } if( timeout > 0 ) { logger.log( LogLevel.DEBUG2, "modbus delay {0}", timeout ); Thread.Sleep( timeout ); } } timeout = calculate_timeout( frame.get_send_cnt() + frame.get_recv_exp(), timeout_additional_ms ); // Console.WriteLine( "timeout: {0}", timeout ); // send the request had_error = true; try { frame.print_msg( "Send", true, logger ); serial.write( frame.get_send_buf(), 0, frame.get_send_cnt() ); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus send error" ); logger.log( LogLevel.WARNING, exc ); // last_transfer = DateTime.UtcNow; return ModbusError.SEND_ERROR; } // wait until a reply has been received or timeout occured logger.log( LogLevel.DEBUG2, "modbus wait for reply {0}", timeout ); logger.log( LogLevel.DEBUG2, "sem.WaitOne..." ); bool got_signal = sem.WaitOne( timeout ); logger.log( LogLevel.DEBUG2, "sem.WaitOne done {0}.", got_signal ); receiver_on = false; if( !got_signal ) { // last_transfer = DateTime.UtcNow; logger.log( LogLevel.DEBUG2, "modbus receive timeout" ); frame.print_msg( "Recv-timeout", false, logger ); // +++ print_msg( "Recv", recv_buf, recv_cnt ); return ModbusError.TIMEOUT; } logger.log( LogLevel.DEBUG2, "modbus received message" ); /* frame.print_msg( "Recv", false, logger ); if( messageLog != null ) { messageLog.logModbusMessage( false, frame.toString( false ) ); } */ return ModbusError.OK; } protected override void communicate_result( ModbusError me ) { last_transfer = DateTime.UtcNow; had_error = me != ModbusError.OK; } private void data_received( byte[] data ) { if( receiver_on ) { if( frame.received_data( data, logger ) ) { try { logger.log( LogLevel.DEBUG2, "sem.Release..." ); sem.Release(); logger.log( LogLevel.DEBUG2, "sem.Release done." ); } catch( SemaphoreFullException exc ) { logger.log( LogLevel.WARNING, "modbus receive sem error (1)" ); logger.log( LogLevel.WARNING, exc ); } } } } public int calculate_timeout( int bytes, int add_millis ) { int timeout = (int)(serial.get_word_duration( bytes ) * 1000); timeout += add_millis; logger.log( LogLevel.DEBUG2, "calculate_timeout({0},{1}) timeout={2}", bytes, add_millis, timeout ); return timeout; } public override void set_timeout_ms( ushort new_timeout_ms ) { logger.log( LogLevel.DEBUG2, "modbus RTU master {0} set_timeout_ms {1} -> {2}", name, timeout_additional_ms, new_timeout_ms ); timeout_additional_ms = new_timeout_ms; } } public interface Calls { ModbusError read_holding_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); ModbusError read_input_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); ModbusError write_single_register( byte slave_id, ushort addr, ushort data ); ModbusError write_multiple_registers( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); ModbusError read_discrete_inputs( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); ModbusError read_coils( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); ModbusError write_single_coil( byte slave_id, ushort addr, bool value ); ModbusError write_multiple_coils( byte slave_id, ushort addr, ushort[] data, int count=-1, int offset=0 ); } public abstract class Slave : Base { protected Dictionary callstab; public Slave( String _name, Frame _frame, Logger _logger, Dictionary? _callstab=null ) : base( _name, _frame, _logger ) { if( _callstab == null ) { callstab = new Dictionary(); } else { callstab = _callstab; } } public void add_calls( int slave_id, Calls calls ) { callstab.Add( slave_id, calls ); } protected void process_message() { } protected abstract void send_message(); } public class SlaveTcp : Slave { private Socket master_sock; private List connections; static IPAddress resolve( string host ) { try { IPAddress[] addrs = new IPAddress[1]; addrs[0] = IPAddress.Parse( host ); return addrs[0]; } catch( Exception ) { IPHostEntry host_info = Dns.GetHostEntry( host ); return host_info.AddressList[0]; } } public SlaveTcp( ushort _port, Logger? _logger=null ) : this( String.Format( "S:0.0.0.0:{0}", _port ), IPAddress.Any, _port, _logger ) { } public SlaveTcp( String _hostname, ushort _port, Logger? _logger=null ) : this( String.Format( "S:{0}:{1}", _hostname, _port ), resolve( _hostname ), _port, _logger ) { } public SlaveTcp( IPAddress _ip, ushort _port, Logger? _logger=null ) : this( String.Format( "S:{0}:{1}", _ip.ToString(), _port ), _ip, _port, _logger ) { } private SlaveTcp( String _name, IPAddress _ip, ushort _port, Logger? _logger=null ) : base( _name, new FrameMbap( false ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.SlaveTcp" ) ) { connections = new List(); master_sock = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); master_sock.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true ); master_sock.Bind( new IPEndPoint( _ip, _port ) ); master_sock.Listen( 10 ); master_sock.BeginAccept( new AsyncCallback( accepted_cb ), this ); } private static void accepted_cb( IAsyncResult ar ) { SlaveTcp? instance = (SlaveTcp?)ar.AsyncState; instance?.accepted( ar ); } private void accepted( IAsyncResult ar ) { Socket socket = master_sock.EndAccept( ar ); if( socket == null ) { logger.log( LogLevel.ERROR, "EndAccept with null socket" ); return; } String cname = String.Format( "SC:{0}", socket.RemoteEndPoint?.ToString() ?? "" ); logger.log( LogLevel.DEBUG2, "new connection {0}", cname ); SlaveTcpConn conn = new SlaveTcpConn( cname, this, socket, logger, callstab ); connections.Add( conn ); } internal void remove_conn( SlaveTcpConn conn ) { connections.Remove( conn ); } protected override void send_message() { // no messages are ever sent on the master socket } } public class SlaveTcpConn : Slave { SlaveTcp parent; Socket socket; public SlaveTcpConn( String _name, SlaveTcp _parent, Socket _socket, Logger _logger, Dictionary _callstab ) : base( _name, new FrameMbap( false ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.SlaveTcp" ), _callstab ) { parent = _parent; socket = _socket; frame.clear_recv_buf( 0 ); recv(); } internal SlaveTcp get_parent() { return parent; } private void close() { } private void recv() { try { logger.log( LogLevel.DEBUG2, "modbus TCP client {0} read(): {1} ({2}/{3})", name, frame.get_recv_req(), frame.get_recv_cnt(), frame.get_recv_exp() ); socket.BeginReceive( frame.get_recv_buf(), frame.get_recv_cnt(), frame.get_recv_req(), 0, new AsyncCallback( received_cb ), this ); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} read():", name ); logger.log( LogLevel.WARNING, exc ); close(); } } private static void received_cb( IAsyncResult ar ) { SlaveTcpConn? instance = (SlaveTcpConn?)ar.AsyncState; instance?.received( ar ); } private void received( IAsyncResult ar ) { try { int n = socket.EndReceive( ar ); if( n == 0 ) { // socket has been closed on the client side logger.log( LogLevel.DEBUG2, "modbus TCP client {0} read_done(): socket closed", name ); close(); } else if( n < 0 ) { // should not happen??? logger.log( LogLevel.DEBUG2, "modbus TCP client {0} read_done(): error reading?", name ); close(); } else { if( frame.received_data( n ) ) { process_message(); } else { recv(); } } } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} read_done():", name ); logger.log( LogLevel.WARNING, exc ); close(); } } protected override void send_message() { frame.print_msg( "Send", true, logger ); if( messageLog != null ) { messageLog.logModbusMessage( true, frame.toString( true ) ); } socket.BeginSend( frame.get_send_buf(), 0, frame.get_send_cnt(), 0, new AsyncCallback( sent_cb ), this ); } private static void sent_cb( IAsyncResult ar ) { SlaveTcpConn? instance = (SlaveTcpConn?)ar.AsyncState; instance?.sent( ar ); } private void sent( IAsyncResult ar ) { try { // Complete sending the data to the remote device. logger.log( LogLevel.DEBUG2, "modbus TCP client {0} sent()", name ); socket.EndSend( ar ); // start receiving recv(); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus TCP client {0} sent():", name ); logger.log( LogLevel.WARNING, exc ); close(); } } } public class SlaveRtu : Slave { private SerPort serial; private bool receiver_on; public SlaveRtu( string port_spec, Logger? _logger=null ) : this( new SerPort( port_spec ), _logger ) { } public SlaveRtu( string port_name, int baud_rate, Parity parity, int data_bits, StopBits stop_bits, Logger? _logger=null ) : this( new SerPort( port_name, baud_rate, parity, data_bits, stop_bits ), _logger ) { } private SlaveRtu( SerPort _serial, Logger? _logger=null ) : base( "S:" + _serial.ToString(), new FrameAdu( false ), _logger != null ? _logger : Logger.get_logger( "BorgCh.Comm.Modbus.SlaveRtu" ) ) { serial = _serial; serial.set_read_callback( data_received ); frame.clear_recv_buf( 0 ); receiver_on = true; } private void data_received( byte[] data ) { if( receiver_on ) { if( frame.received_data( data, logger ) ) { frame.print_msg( "Recv", false, logger ); if( messageLog != null ) { messageLog.logModbusMessage( false, frame.toString( false ) ); } process_message(); frame.clear_recv_buf( 0 ); } } } protected override void send_message() { try { frame.print_msg( "Send", true, logger ); if( messageLog != null ) { messageLog.logModbusMessage( true, frame.toString( true ) ); } serial.write( frame.get_send_buf(), 0, frame.get_send_cnt() ); } catch( Exception exc ) { logger.log( LogLevel.WARNING, "modbus send error" ); logger.log( LogLevel.WARNING, exc ); // last_transfer = DateTime.UtcNow; // return ModbusError.SEND_ERROR; } } } } // namespace BorgCh.Comm.Modbus