tbf/TBF/Rig/Modbus/Common/Modbus.cs

401 lines
16 KiB
C#

///
/// 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
{
/// <summary>
/// Root component for Modbus communication via serial port (RS485)
/// </summary>
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;
/// <summary>
/// Enumeration of modbus components via static fields and methods
/// </summary>
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<TelegramMeno> telegramsToSend;
/// An array of queues of received telegrams, one queue for each modbus address
/// Queue<byte[]>[0] is erserved for Groch temperature meters
public Queue<byte[]>[] ReceivedTelegrams { get { return receivedTelegrams; } }
Queue<byte[]>[] receivedTelegrams;
public string[] ComponentNames { get { return componentNames; } }
string[] componentNames;
int registeredDevicesCount; /// Number of devices registered for poling
int firstScheduledTicketNo;
public Modbus() { }
/// <summary>
/// Modbus root component sending and receiving messages via serial port
/// </summary>
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<TelegramMeno>();
receivedTelegrams = new Queue<byte[]>[256];
for (int i = 0; i < receivedTelegrams.Length; i++) receivedTelegrams[i] = new Queue<byte[]>();
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);
}
}
/// <summary>
/// Registers a modbus device for scheduled polling
/// </summary>
/// <returns>Ticket numeber to be used as IsMyTurn() argumenr</returns>
public int RegisterForPolling()
{
registeredDevicesCount++;
return registeredDevicesCount - 1; /// 0 .. number of registered devices - 1
}
/// <summary>
/// Determines whether the registered device should do polling now
/// </summary>
/// <param name="ticketNr">Ticket number in range 0 .. registered devs.count - 1 returned by RegisterForPolling()</param>
/// <returns>true when yes</returns>
public bool IsMyTurn(int ticketNr)
{
return ((ticketNr >= firstScheduledTicketNo) && (ticketNr < firstScheduledTicketNo + RegTelegramsPerSecond))
|| (ticketNr < firstScheduledTicketNo + RegTelegramsPerSecond - registeredDevicesCount);
}
/// <summary>
/// Send a structured modbus message.
/// </summary>
/// <param name="modbusAddress">Device address (1..255) or 0 = broadcast</param>
/// <param name="function">Function (0..127)</param>
/// <param name="dataAddress">Address of data to be transferred (0..65535)</param>
/// <param name="dataCount">Count of data bytes to be transferred (0..65535)</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="message">Message incl CRC fields, CRC bytes dont have to be set</param>
public void SendMessage(byte[] message, string meno)
{
Telegram.UpdateTelegramCRC(message);
telegramsToSend.Enqueue(new TelegramMeno(message, meno));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="message">Message incl CRC fields, CRC bytes dont have to be set</param>
public void SendMessageGroch(byte[] message, string meno)
{
telegramsToSend.Enqueue(new TelegramMeno(message, meno));
}
/// <summary>Run this device</summary>
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;
}
/// <summary>Run this device</summary>
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);
}
/// <summary>Stop this device</summary>
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);
}
}
}