401 lines
16 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|