tbf/TBF/Rig/Modbus/Common/Modbus.cs
2021-04-03 00:54:18 +02:00

317 lines
13 KiB
C#

///
/// Copyright (c) 2015-2021 Sensus Metering Systems
///
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("Modbus({0})", Cfg.ToString(-1)); }
const int MaxTelegramsPerSecond = 5; /// 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;
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<byte[]> 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;
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;
log.Warn(this.ToString());
}
public override void Initialize()
{
telegramsToSend = new Queue<byte[]>();
receivedTelegrams = new Queue<byte[]>[256];
for (int i = 0; i < receivedTelegrams.Length; i++)
{
receivedTelegrams[i] = new Queue<byte[]>();
}
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 });
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} - Device successfully initialized", Name);
}
else
{
serialPort = null;
log.FatalFormat("{0} - Device simulated", Name);
}
}
/// <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)
{
SendMessage(new byte[8] { modbusAddress, function, (byte)(dataAddress / 256), (byte)(dataAddress % 256),
(byte)(dataCount / 256), (byte)(dataCount % 256), 0, 0 });
}
/// <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)
{
Telegram.UpdateTelegramCRC(message);
telegramsToSend.Enqueue(message);
}
/// <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)
{
telegramsToSend.Enqueue(message);
}
/// <summary>Run this device</summary>
public void RunDeviceBefore()
{
/// Advance 'firstScheduledTicketNo' variable used inside IsMyTurn(int tickeNo)
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));
}
///
string st = string.Format("{0}", Name, Telegram.LogTelegram("Received data ", buffer, 0, bytesCount));
Debug.WriteLine(st);
log.Debug(st);
///
for (int i = 0; i < bytesCount && rcvdBytesCount < BUFFER_SIZE; i++, rcvdBytesCount++)
{
rcvdData[rcvdBytesCount] = buffer[i];
}
}
/// 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} queue size={1}", Telegram.LogTelegram("Received message ", 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} queue size={1}", Telegram.LogTelegram("Received message ", 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()
{
int telegramsToSendNow = Math.Min(MaxTelegramsPerSecond, telegramsToSend.Count);
for (int i = 0; i < telegramsToSendNow; i++, Thread.Sleep(100))
{
WriteMessageToSerialPort(telegramsToSend.Dequeue());
}
}
/// <summary>Stop this device</summary>
public void StopDevice()
{
/// Send telegrams currently in the queue
while (telegramsToSend.Count > 0)
{
Thread.Sleep(100);
WriteMessageToSerialPort(telegramsToSend.Dequeue());
}
if (serialPort != null && serialPort.IsOpen) serialPort.Close();
serialPort = null;
}
public void StopDevice2() { }
public void WriteMessageToSerialPort(byte[] message)
{
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 ", Name), message);
Debug.WriteLine(s);
log.Debug(s);
}
}
}