/// /// Copyright (c) 2015-2021 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; using System.Threading; using Common; using Config.Entities; using log4net; using TBF.Rig.Generic; namespace TBF.Rig.Modbus.Common { /// /// Root component for Modbus communication via serial port (RS485) /// public class Modbus : ComponentBase, IDevice, GenericDevices.IModbus { private static readonly ILog log = LogManager.GetLogger(typeof(Modbus)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } const int MaxTelegramsPerSecond = 6; /// Max. number of telegrams to be sent per second (100 ms for each telegram) const int RegTelegramsPerSecond = 2; /// Number of registered polling telegrams per second const int MinTelegramLen = 4; /// /// Enumeration of modbus components via static fields and methods /// static int nextIdx = 0; static int modbusComponentsCount { get { return nextIdx; } } static Modbus[] modbusComponents; /// int modbusComponentIx; /// 0-based modbus component index assigned in Initialize() private readonly ModbusCfg modbusCfg; /// Serial port and received data buffers SerialPort serialPort; const int BUFFER_SIZE = 1000; /// Size of input buffers byte[] buffer; /// Buffer for data received in one attempt in RunDeviceBefore() byte[] rcvdData; /// Buffer for all recevied but nut processed data. May contain data from several read attempts int rcvdBytesCount; /// Count of valid bytes in rcvdData buffer /// Queue for all telegrams to be sent by the serial port Queue telegramsToSend; /// An array of queues of received telegrams, one queue for each modbus address /// Queue[0] is erserved for Groch temperature meters public Queue[] ReceivedTelegrams { get { return receivedTelegrams; } } Queue[] receivedTelegrams; public string[] ComponentNames { get { return componentNames; } } string[] componentNames; int registeredDevicesCount; /// Number of devices registered for poling int firstScheduledTicketNo; public Modbus() { } /// /// Modbus root component sending and receiving messages via serial port /// public Modbus(Generic.IComponentCfg cfg) : base(cfg) { modbusCfg = cfg as ModbusCfg; } public override void Initialize() { modbusComponentIx = nextIdx++; /// if (modbusComponents == null || modbusComponents.Length < nextIdx) { Modbus[] componentsSoFar = modbusComponents; modbusComponents = new Modbus[modbusComponentsCount]; if (componentsSoFar != null) { for (int i = 0; i < componentsSoFar.Length; i++) modbusComponents[i] = componentsSoFar[i]; } modbusComponents[modbusComponentsCount - 1] = this; } telegramsToSend = new Queue(); receivedTelegrams = new Queue[256]; for (int i = 0; i < receivedTelegrams.Length; i++) receivedTelegrams[i] = new Queue(); componentNames = new string[256]; for (int i = 0; i < componentNames.Length; i++) componentNames[i] = "?"; buffer = new byte[BUFFER_SIZE]; rcvdData = new byte[BUFFER_SIZE]; rcvdBytesCount = 0; registeredDevicesCount = 0; firstScheduledTicketNo = 0; /// Insert 'Search Quido RS modules' message into the queue /// Causes search for Quido RS modules in the first RunDeviceAfter() call SendMessage(new byte[] { 0xF8, 0x11, 0, 0 }, "all"); if (modbusCfg.DebugLevel == DebugMode.Normal) { string comPortName = "COM" + modbusCfg.ComPortNr.ToString(); serialPort = new SerialPort(comPortName, modbusCfg.BaudRate, modbusCfg.Parity, modbusCfg.DataBits, modbusCfg.StopBits); serialPort.Handshake = modbusCfg.Handshake; serialPort.Open(); log.FatalFormat("{0} initialized: {1}", Name, this); } else { serialPort = null; log.FatalFormat("{0} simulated: {1}", Name, this); } } /// /// Registers a modbus device for scheduled polling /// /// Ticket numeber to be used as IsMyTurn() argumenr public int RegisterForPolling() { registeredDevicesCount++; return registeredDevicesCount - 1; /// 0 .. number of registered devices - 1 } /// /// Determines whether the registered device should do polling now /// /// Ticket number in range 0 .. registered devs.count - 1 returned by RegisterForPolling() /// true when yes public bool IsMyTurn(int ticketNr) { return ((ticketNr >= firstScheduledTicketNo) && (ticketNr < firstScheduledTicketNo + RegTelegramsPerSecond)) || (ticketNr < firstScheduledTicketNo + RegTelegramsPerSecond - registeredDevicesCount); } /// /// Send a structured modbus message. /// /// Device address (1..255) or 0 = broadcast /// Function (0..127) /// Address of data to be transferred (0..65535) /// Count of data bytes to be transferred (0..65535) public void SendMessage(byte modbusAddress, byte function, ushort dataAddress, ushort dataCount, string meno) { SendMessage(new byte[8] { modbusAddress, function, (byte)(dataAddress / 256), (byte)(dataAddress % 256), (byte)(dataCount / 256), (byte)(dataCount % 256), 0, 0 }, meno); } /// /// Send an arbitrary modbus message. /// When the message length is N, however only bytes 1..N-2 have to be set. /// The last two message bytes (CRC) may be uninitialized or zero. /// They are calculated inside this function as required by Modbus specification. /// /// Message incl CRC fields, CRC bytes dont have to be set public void SendMessage(byte[] message, string meno) { Telegram.UpdateTelegramCRC(message); telegramsToSend.Enqueue(new TelegramMeno(message, meno)); } /// /// Send an arbitrary modbus message. /// When the message length is N, however only bytes 1..N-2 have to be set. /// The last two message bytes (CRC) may be uninitialized or zero. /// They are calculated inside this function as required by Modbus specification. /// /// Message incl CRC fields, CRC bytes dont have to be set public void SendMessageGroch(byte[] message, string meno) { telegramsToSend.Enqueue(new TelegramMeno(message, meno)); } /// Run this device public void RunDeviceBefore() { /// Advance 'firstScheduledTicketNo' variable used inside IsMyTurn(int tickeNo) if (registeredDevicesCount > 0) { firstScheduledTicketNo = (firstScheduledTicketNo + RegTelegramsPerSecond) % registeredDevicesCount; } if (modbusCfg.DebugLevel == DebugMode.Normal && serialPort != null && serialPort.IsOpen) { /// /// Read new received bytes and append them to a buffer /// int bytesCount = serialPort.BytesToRead; if (bytesCount > 0) { bytesCount = serialPort.Read(buffer, 0, Math.Min(bytesCount, BUFFER_SIZE)); /// for (int i = 0; i < bytesCount && rcvdBytesCount < BUFFER_SIZE; i++, rcvdBytesCount++) { rcvdData[rcvdBytesCount] = buffer[i]; } /// string st = string.Format("[{0}] received {1}", Name, Telegram.LogTelegram("", buffer, 0, bytesCount)); Debug.WriteLine(st); log.Debug(st); } } /// Try to detect various kinds of data in rcvdData buffer in a loop, start at offset 0 int offset = 0; int lastValidOffset = 0; /// Offset of the first byte after the last successfully parsed frame do { bool anyProcessed = false; if (offset + 28 <= rcvdBytesCount && rcvdData[offset] == '\r' && rcvdData[offset + 1] == '\n' && rcvdData[offset + 3] == 28 && rcvdData[offset + 5] == 2) { int sum = 0; for (int i = offset; i < offset + 27; i++) sum += rcvdData[i]; /// if (rcvdData[offset + 27] == (sum & 0xFF)) { /// Copy data to a created telegram byte[] receivedTelegram = new byte[28]; for (int i = 0; i < 28; i++) receivedTelegram[i] = rcvdData[offset + i]; /// Enqueue receivedTelegrams[0].Enqueue(receivedTelegram); string s = string.Format("[{0}] received message from [{1}] {2} queue size={3}", Name, componentNames[0], Telegram.LogTelegram("", receivedTelegram), receivedTelegrams[0].Count); Debug.WriteLine(s); log.Debug(s); offset += 28; lastValidOffset = offset; anyProcessed = true; } } for (int candidateLen = 4; candidateLen <= rcvdBytesCount - offset; candidateLen++) { if (Telegram.VerifyTelegramCRC(offset, rcvdData, candidateLen)) { /// A valid telegram found in dataToProcess buffer /// Copy data to a created telegram byte[] receivedTelegram = new byte[candidateLen]; for (int i = 0; i < candidateLen; i++) receivedTelegram[i] = rcvdData[offset + i]; /// Enqueue int deviceAddress = receivedTelegram[0]; receivedTelegrams[deviceAddress].Enqueue(receivedTelegram); string s = string.Format("[{0}] received message from [{1}] {2} queue size={3}", Name, componentNames[deviceAddress], Telegram.LogTelegram("", receivedTelegram), receivedTelegrams[deviceAddress].Count); Debug.WriteLine(s); log.Debug(s); offset += candidateLen; lastValidOffset = offset; anyProcessed = true; } } if (!anyProcessed) offset++; } while (offset <= rcvdBytesCount - 4); /// Remove processed data from the receive buffer for (int i = lastValidOffset; i < rcvdBytesCount; i++) { rcvdData[i - lastValidOffset] = rcvdData[i]; } rcvdBytesCount -= lastValidOffset; } /// Run this device public void RunDeviceAfter() { telegramsToSendInThisTick = Math.Min(MaxTelegramsPerSecond, telegramsToSend.Count); if (modbusComponentIx == 0) { int maxTelegramsToSendInThisTick = telegramsToSendInThisTick; for (int i = 1; i < modbusComponentsCount; i++) { if (modbusComponents[i].telegramsToSendInThisTick > maxTelegramsToSendInThisTick) { maxTelegramsToSendInThisTick = modbusComponents[i].telegramsToSendInThisTick; } } for (int t = 0; t < maxTelegramsToSendInThisTick; t++, Thread.Sleep(100)) { for (int i = 0; i < modbusComponentsCount; i++) { if (t < modbusComponents[i].telegramsToSendInThisTick) { modbusComponents[i].WriteOneMessageToSerialPort(); } } } } } /// int telegramsToSendInThisTick; /// void WriteOneMessageToSerialPort() { TelegramMeno msg = telegramsToSend.Dequeue(); WriteMessageToSerialPort(msg.Telegram, msg.Meno, telegramsToSend.Count); } /// Stop this device public void StopDevice() { telegramsToSendInThisTick = telegramsToSend.Count; /// Send all telegrams currently in all queues if (modbusComponentIx == 0) { int maxTelegramsToSendInThisTick = telegramsToSendInThisTick; for (int i = 1; i < modbusComponentsCount; i++) { if (modbusComponents[i].telegramsToSendInThisTick > maxTelegramsToSendInThisTick) { maxTelegramsToSendInThisTick = modbusComponents[i].telegramsToSendInThisTick; } } Thread.Sleep(100); for (int t = 0; t < maxTelegramsToSendInThisTick; t++, Thread.Sleep(100)) { for (int i = 0; i < modbusComponentsCount; i++) { if (t < modbusComponents[i].telegramsToSendInThisTick) { modbusComponents[i].WriteOneMessageToSerialPort(); } } } } if (serialPort != null && serialPort.IsOpen) serialPort.Close(); serialPort = null; } public void StopDevice2() { } public void WriteMessageToSerialPort(byte[] message, string meno, int queueCount) { if (modbusCfg.DebugLevel == DebugMode.Normal) { /// Make sure the serial port is open if (serialPort == null || !serialPort.IsOpen) { /// Serial port is closed or does not exist => re-open the serial port try { if (serialPort == null) { string comPortName = "COM" + modbusCfg.ComPortNr.ToString(); serialPort = new SerialPort(comPortName, modbusCfg.BaudRate, modbusCfg.Parity, modbusCfg.DataBits, modbusCfg.StopBits); } serialPort.Handshake = modbusCfg.Handshake; serialPort.Open(); } catch (Exception) { /// Re-opening serial port failed return; } } serialPort.Write(message, 0, message.Length); } string s = Telegram.LogTelegram(string.Format("[{0}] sending message to [{1}] ", Name, meno), message); Debug.WriteLine(s); log.Debug(s); } } }