317 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|