/******************************************************************************* * * * Société de Transports de Montréal. * * 2012 - 2013 * * * * Projet Zones Tests * * * * * * * *******************************************************************************/ /* Description: Classe qui implémente le protocole Modbus. */ /* ************************************************************************** */ /* Revision: ### YYYMMDD JFM Verision d'origine. ### YYYYMMDD Description du besoin ou du bug Description du changement. */ /* ************************************************************************** */ #include "ModbusBackend.h" #include "QBuffer" #include "EngLog.h" #include "ZTLog.h" CModbusBackend::CModbusBackend(CModbusRepository *Repo) { mModbusTCPSocketHandle = 0; mDataLinkValid = false; mModbusRepo = Repo; mModbusMode = MODBUS_INVALID_MODE; mTransactionIDCounter = 0; mDeviceID = 9;//0xFF; mModbusMaxRetry = MODBUS_RETRY_MAX_COUNT; mModbusRequestTimeout = MODBUS_RETRY_DELAY; } CModbusBackend::~CModbusBackend() { for(int i= 0; i < mRequestsList.size(); i++) { delete mRequestsList[i]; } mRequestsList.clear(); } void CModbusBackend::ModbusDataReady() { CModbusTransaction Transaction; QByteArray InData = mModbusTCPSocketHandle->readAll(); QBuffer FileBuffer(&InData); FileBuffer.open(QIODevice::ReadOnly); FileBuffer.seek(0); QDataStream *TransactionDataStrm = new QDataStream(&FileBuffer); *TransactionDataStrm >> Transaction.mHeader; *TransactionDataStrm >> Transaction.mPDU.mFunctionCode; Transaction.mPDU.mData = InData.right(Transaction.mHeader.mMessageLength - 2); //-2 to remove Device ID and Function Code. // // qDebug("---------------------------------------"); // qDebug("modbus data received %s",InData.toHex().data()); // qDebug("Transaction ID 0x%X",Transaction.mHeader.mTransactionID); // qDebug("Message Length %d",Transaction.mHeader.mMessageLength); // qDebug("Protocol ID 0x%X",Transaction.mHeader.mProtocolID); // qDebug("Unit ID 0x%X",Transaction.mHeader.mUnitID); // qDebug("Function Code 0x%X",Transaction.mPDU.mFunctionCode); // qDebug("Data %s",Transaction.mPDU.mData.toHex().data()); // qDebug("---------------------------------------"); if(mModbusMode == MODBUS_MASTER_MODE) { AnalyzeModbusResponse(Transaction); //Le maître recoit des réponses } else if( mModbusMode == MODBUS_SLAVE_MODE) { AnalyzeModbusRequest(Transaction); //L'esclave recoit des requêtes } else { CEngLog::instance()->AddLogString("Erreur Modbus: "); //Erreur de logique interne. Ne doit pas survenir normalement } delete TransactionDataStrm; } void CModbusBackend::ModbusLinkDisconnected() { // qDebug("Modbus link disconnected"); mDataLinkValid = false; mModbusTCPSocketHandle->flush(); } //In client mode. This is the request from the master. int CModbusBackend::AnalyzeModbusRequest(CModbusTransaction Transaction) { if(Transaction.mHeader.mProtocolID != 0) { //Invalid protocol... what can we do? return 0; } switch(Transaction.mPDU.mFunctionCode) { case MODBUS_FCT_READ_HOLDING_REGISTERS: { bool ok = true; unsigned short StartAdress = 0; StartAdress = Transaction.mPDU.mData[0]&0xFF; StartAdress <<= 8; StartAdress += Transaction.mPDU.mData[1]&0xFF; unsigned short NbRegisters = 0; NbRegisters = Transaction.mPDU.mData[2]&0xFF; NbRegisters <<= 8; NbRegisters += Transaction.mPDU.mData[3]&0xFF; //Validate nb of registers if(NbRegisters < 1 || NbRegisters > MODBUS_MAX_NB_REGISTERS) { SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,MODBUS_FCT_READ_HOLDING_REGISTERS); //Fonction à être implémentée par le parent return 0; } //Validate data range if(!mModbusRepo->IsHRValid(StartAdress,NbRegisters)) { qDebug("Reg invalid"); //Send negative response SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS,MODBUS_FCT_READ_HOLDING_REGISTERS); return 0; } QByteArray data = mModbusRepo->GetHRData(StartAdress,NbRegisters,&ok); // qDebug("Slave Rx Read Holding Registers. Address: %d, Nb Reg: %d",StartAdress, NbRegisters); // qDebug("Data: %s",data.toHex().data()); //The response to a HR reading needs the byte count before the data. quint8 ByteCount = data.size(); data.prepend(ByteCount); SendModbusResponse(Transaction, data); //All OK break; } case MODBUS_WRITE_SINGLE_REGISTER: { unsigned short StartAdress = 0; StartAdress = Transaction.mPDU.mData[0]&0xFF; StartAdress <<= 8; StartAdress += Transaction.mPDU.mData[1]&0xFF; //Validate data range if(!mModbusRepo->IsHRValid(StartAdress,1)) { qDebug("Reg invalid"); //Send negative response SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS,MODBUS_WRITE_SINGLE_REGISTER); return 0; } //Extract data. QByteArray data = Transaction.mPDU.mData.right(2); //Write register data mModbusRepo->WriteHRData(StartAdress,1,data); if(StartAdress == 2000) { qDebug("debug... 2000"); } // qDebug("Slave Rx Write Single Register. Address: %d, Value: 0x%s",StartAdress, data.toHex().data()); // qDebug("Data: %s",data.toHex().data()); data = Transaction.mPDU.mData.left(4); //The response corresponds to the Reg. Address & the value. Which is the first 4 bytes of the initial request. SendModbusResponse(Transaction, data); RegistersDatabaseUpdated(StartAdress,1); break; } case MODBUS_FCT_WRITE_MULTIPLE_REGISTERS: { unsigned short StartAdress = 0; StartAdress = Transaction.mPDU.mData[0]&0xFF; StartAdress <<= 8; StartAdress += Transaction.mPDU.mData[1]&0xFF; unsigned short NbRegisters = 0; NbRegisters = Transaction.mPDU.mData[2]&0xFF; NbRegisters <<= 8; NbRegisters += Transaction.mPDU.mData[3]&0xFF; quint8 ByteCount = Transaction.mPDU.mData[4]; //Validate nb of registers if(NbRegisters < 1 || NbRegisters > 0x7D || ByteCount != (NbRegisters*2)) { qDebug("Invalid register number or byte count "); SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,MODBUS_FCT_WRITE_MULTIPLE_REGISTERS); return 0; } //Validate data range if(!mModbusRepo->IsHRValid(StartAdress,NbRegisters)) { qDebug("Reg invalid"); //Send negative response SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS,MODBUS_FCT_WRITE_MULTIPLE_REGISTERS); return 0; } //Extract data. QByteArray data = Transaction.mPDU.mData.right(ByteCount); //Write register data mModbusRepo->WriteHRData(StartAdress,NbRegisters,data); qDebug("Slave Rx Write Multiple Registers. Address: %d, Nb Reg: %d",StartAdress, NbRegisters); qDebug("Data: %s",data.toHex().data()); data = Transaction.mPDU.mData.left(4); //The response corresponds to the Start Adress and Nb of Regs. Which is the first 4 bytes of the initial request. SendModbusResponse(Transaction, data); RegistersDatabaseUpdated(StartAdress,NbRegisters); break; } default: { //Received "Illegal function code". Send the exception code to master //TODO: Log this. qDebug("Slave received illegal function code from master: 0x%x",Transaction.mPDU.mFunctionCode); SendErrorResponse(Transaction,MODBUS_EXCEPTION_ILLEGAL_FCT); ModbusRequestException(MODBUS_EXCEPTION_ILLEGAL_FCT,Transaction.mPDU.mFunctionCode); break; } } return 1; } int CModbusBackend::SendModbusResponse(CModbusTransaction RequestTransaction, QByteArray Data) { QByteArray ModbusPacket; QBuffer Buffer(&ModbusPacket); Buffer.open(QIODevice::WriteOnly|QIODevice::Unbuffered); Buffer.seek(0); QDataStream *PacketDataStrm = new QDataStream(&Buffer); //For a response, the header will be the same as the original request, except for the msg. length. //Set the appropriate msg length. RequestTransaction.mHeader.mMessageLength = Data.size() + 2; //+2 to add function code & Unit ID. RequestTransaction.mPDU.mData = Data; *PacketDataStrm << RequestTransaction.mHeader; *PacketDataStrm << RequestTransaction.mPDU.mFunctionCode; Buffer.close(); ModbusPacket.append(Data); // qDebug("Response packet: %s",ModbusPacket.toHex().data()); mModbusTCPSocketHandle->write(ModbusPacket); delete PacketDataStrm; return RET_OK; } //In Master mode. This is the response from slave to a previously sent request. int CModbusBackend::AnalyzeModbusResponse(CModbusTransaction Transaction) { if(Transaction.mHeader.mProtocolID != 0) { //Invalid protocol... what can we do? return RET_ERROR; } //Find matching request and remove it from the queue... CModbusRequest *Request; bool Found = false; for(int i = 0; i < mRequestsList.size(); i++) { if(mRequestsList.at(i)->mHeader.mTransactionID == Transaction.mHeader.mTransactionID) { Request = mRequestsList.takeAt(i); //Remove from queue and keep a copy Request->mRequestTimer->stop(); //Stop the resend timer Found = true; } } if(Found == false) { //Invalid request number. This should happen only if a very long delay exists in the comm. //TODO: Log this... qDebug("Master received response to a non existent request!!!"); return RET_ERROR; } //check if we have an exception response if((Transaction.mPDU.mFunctionCode & MODBUS_EXCEPTION_FCT_MASK) != 0) { //we have an exception response... something went wrong. quint8 ExceptionCode = Transaction.mPDU.mData[0]; //TODO: Manage this! qDebug("Master Rx exception code %d to request %d",ExceptionCode,Request->mPDU.mFunctionCode); emit ModbusResponseException(ExceptionCode,Request->mPDU.mFunctionCode); delete Request; return RET_ERROR; } switch(Transaction.mPDU.mFunctionCode) { case MODBUS_FCT_READ_HOLDING_REGISTERS: { quint8 ByteCount = 0; ByteCount = Transaction.mPDU.mData.at(0); if((Request->mNbRegisters*2) != ByteCount) { //Inconsistency between the data range and the data count. //TODO: Log the error. qDebug("Master eceived a wrong data size in response for a MODBUS_FCT_READ_HOLDING_REGISTERS request"); emit ModbusResponseException(MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,MODBUS_FCT_READ_HOLDING_REGISTERS); delete Request; return RET_ERROR; } QByteArray RegisterValues = Transaction.mPDU.mData.right(ByteCount); qDebug("Master Rx Read Holding Registers Response."); qDebug("Data: %s",RegisterValues.toHex().data()); mModbusRepo->WriteHRData(Request->mStartAddress,Request->mNbRegisters,RegisterValues); RegistersDatabaseUpdated(Request->mStartAddress, Request->mNbRegisters); break; } case MODBUS_WRITE_SINGLE_REGISTER: { quint16 RegAddress = 0; RegAddress = Transaction.mPDU.mData[0]&0xFF; RegAddress <<= 8; RegAddress += Transaction.mPDU.mData[1]&0xFF; if(Request->mStartAddress != RegAddress) { //Inconsistency between the request Adress and response Adress. //TODO: Log the error. qDebug("Master received a wrong Register Adress in response for a MODBUS_WRITE_SINGLE_REGISTER request"); emit ModbusResponseException(MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,MODBUS_WRITE_SINGLE_REGISTER); delete Request; return RET_ERROR; } qDebug("Master Rx Write Single Register response. Address: %d,",RegAddress); qDebug("Data: %s",Transaction.mPDU.mData.toHex().data()); //Everything seems good. break; } case MODBUS_FCT_WRITE_MULTIPLE_REGISTERS: { unsigned short StartAdress = 0; StartAdress = Transaction.mPDU.mData[0]&0xFF; StartAdress <<= 8; StartAdress += Transaction.mPDU.mData[1]&0xFF; unsigned short NbRegisters = 0; NbRegisters = Transaction.mPDU.mData[2]&0xFF; NbRegisters <<= 8; NbRegisters += Transaction.mPDU.mData[3]&0xFF; if(StartAdress != Request->mStartAddress || NbRegisters != Request->mNbRegisters) { //Inconsistency between the request Adress or NbRegisters and response. //TODO: Log the error. qDebug("Master Received a wrong Register Adress or NbRegisters in response for a MODBUS_FCT_WRITE_MULTIPLE_REGISTERS request"); emit ModbusResponseException(MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,MODBUS_FCT_WRITE_MULTIPLE_REGISTERS); delete Request; return RET_ERROR; } qDebug("Master Rx Write Multiple Registers response. Address: %d, Nb Reg: %d",StartAdress, NbRegisters); qDebug("Data: %s",Transaction.mPDU.mData.toHex().data()); //All is good. break; } default: { //Received "Illegal function code" response //TODO: Log this. qDebug("Master received illegal function code 0x%x",Transaction.mPDU.mFunctionCode); emit ModbusResponseException(MODBUS_EXCEPTION_ILLEGAL_FCT,MODBUS_FCT_WRITE_MULTIPLE_REGISTERS); break; } } delete Request; return 1; } int CModbusBackend::SendModbusRequest(CModbusRequest *Request) { QByteArray ModbusPacket; QBuffer Buffer(&ModbusPacket); Buffer.open(QIODevice::WriteOnly|QIODevice::Unbuffered); Buffer.seek(0); QDataStream *PacketDataStrm = new QDataStream(&Buffer); *PacketDataStrm << Request->mHeader; *PacketDataStrm << Request->mPDU.mFunctionCode; Buffer.close(); ModbusPacket.append(Request->mPDU.mData); qDebug("Request packet: %s",ModbusPacket.toHex().data()); mModbusTCPSocketHandle->write(ModbusPacket); delete PacketDataStrm; return RET_OK; } int CModbusBackend::SendErrorResponse(CModbusTransaction RequestTransaction, quint8 ErrorCode) { QByteArray ModbusPacket; QBuffer Buffer(&ModbusPacket); Buffer.open(QIODevice::WriteOnly|QIODevice::Unbuffered); Buffer.seek(0); QDataStream *PacketDataStrm = new QDataStream(&Buffer); //For a response, the header will be the same as the original request, except for the msg. length. //Set the appropriate msg length. RequestTransaction.mHeader.mMessageLength = 3; //Unit ID, function code & Exception code. *PacketDataStrm << RequestTransaction.mHeader; Buffer.close(); ModbusPacket.append(RequestTransaction.mPDU.mFunctionCode + 0x80); ModbusPacket.append(ErrorCode); CEngLog::instance()->AddLogString(QString("Modbus: Envoi d'un code d'un code d'erreur. Error Code %1, Error Packet: %2").arg(ErrorCode).arg(ModbusPacket.toHex().data())); // qDebug("Sending error code %d. Error packet: %s",ErrorCode,ModbusPacket.toHex().data()); mModbusTCPSocketHandle->write(ModbusPacket); delete PacketDataStrm; return RET_OK; } int CModbusBackend::SendReadHoldingRegistersRequest(quint16 StartAddress, quint16 RegisterCount) { //First, validate that the reading range is within our repo if(mModbusRepo->IsHRValid(StartAddress,RegisterCount) == false) { CEngLog::instance()->AddLogString("Trying to send a read HR in an invalid range"); return RET_ERROR; } //Create a request. CModbusRequest *NewRequest = new CModbusRequest; NewRequest->mStartAddress = StartAddress; NewRequest->mNbRegisters = RegisterCount; connect(NewRequest->mRequestTimer,SIGNAL(timeout()),this,SLOT(RequestTimerExpired())); NewRequest->mPDU.mData.clear(); NewRequest->mPDU.mFunctionCode = MODBUS_FCT_READ_HOLDING_REGISTERS; quint8 HighByte, LowByte; LowByte = StartAddress & 0x00FF; HighByte = (StartAddress >> 8) & 0x00FF; NewRequest->mPDU.mData.append(HighByte); NewRequest->mPDU.mData.append(LowByte); LowByte = RegisterCount & 0x00FF; HighByte = (RegisterCount >> 8) & 0x00FF; NewRequest->mPDU.mData.append(HighByte); NewRequest->mPDU.mData.append(LowByte); NewRequest->mHeader.mMessageLength = NewRequest->mPDU.mData.size() + 2; NewRequest->mHeader.mProtocolID = 0; NewRequest->mHeader.mTransactionID = (qint16)GetNewTransactionID(); NewRequest->mHeader.mUnitID = mDeviceID; mRequestsList.append(NewRequest); SendModbusRequest(NewRequest); NewRequest->mRequestTimer->start(mModbusRequestTimeout); return RET_OK; } int CModbusBackend::SendWriteHoldingRegistersRequest(quint16 StartAddress, quint16 RegisterCount) { //First, validate that the reading range is within our repo if(mModbusRepo->IsHRValid(StartAddress,RegisterCount) == false) { CEngLog::instance()->AddLogString("Trying to send a Write HR in an invalid range SendWriteHoldingRegistersRequest()"); return RET_ERROR; } if(RegisterCount > MODBUS_MAX_NB_REGISTERS) { return RET_ERROR; } //Get data. bool OK; QByteArray RegData = mModbusRepo->GetHRData(StartAddress,RegisterCount,&OK); if(OK == false) { return RET_ERROR; } //Create a request. CModbusRequest *NewRequest = new CModbusRequest; NewRequest->mStartAddress = StartAddress; NewRequest->mNbRegisters = RegisterCount; connect(NewRequest->mRequestTimer,SIGNAL(timeout()),this,SLOT(RequestTimerExpired())); NewRequest->mPDU.mData.clear(); NewRequest->mPDU.mFunctionCode = MODBUS_FCT_WRITE_MULTIPLE_REGISTERS; quint8 HighByte, LowByte; LowByte = StartAddress & 0x00FF; HighByte = (StartAddress >> 8) & 0x00FF; NewRequest->mPDU.mData.append(HighByte); NewRequest->mPDU.mData.append(LowByte); LowByte = RegisterCount & 0x00FF; HighByte = (RegisterCount >> 8) & 0x00FF; NewRequest->mPDU.mData.append(HighByte); NewRequest->mPDU.mData.append(LowByte); NewRequest->mPDU.mData.append(RegData); NewRequest->mHeader.mMessageLength = NewRequest->mPDU.mData.size() + 2; NewRequest->mHeader.mProtocolID = 0; NewRequest->mHeader.mTransactionID = (qint16)GetNewTransactionID(); NewRequest->mHeader.mUnitID = mDeviceID; mRequestsList.append(NewRequest); SendModbusRequest(NewRequest); NewRequest->mRequestTimer->start(mModbusRequestTimeout); return RET_OK; } int CModbusBackend::SendWriteSingleRegisterRequest(quint16 Address) { //First, validate that the reading range is within our repo if(mModbusRepo->IsHRValid(Address,1) == false) { CEngLog::instance()->AddLogString("Trying to send a Write HR in an invalid range SendWriteSingleRegisterRequest()"); return RET_ERROR; } //Get data. bool OK; QByteArray RegData = mModbusRepo->GetHRData(Address,1,&OK); if(OK == false) { return RET_ERROR; } //Create a request. CModbusRequest *NewRequest = new CModbusRequest; NewRequest->mStartAddress = Address; NewRequest->mNbRegisters = 1; connect(NewRequest->mRequestTimer,SIGNAL(timeout()),this,SLOT(RequestTimerExpired())); NewRequest->mPDU.mData.clear(); NewRequest->mPDU.mFunctionCode = MODBUS_WRITE_SINGLE_REGISTER; quint8 HighByte, LowByte; LowByte = Address & 0x00FF; HighByte = (Address >> 8) & 0x00FF; NewRequest->mPDU.mData.append(HighByte); NewRequest->mPDU.mData.append(LowByte); NewRequest->mPDU.mData.append(RegData); NewRequest->mHeader.mMessageLength = NewRequest->mPDU.mData.size() + 2; NewRequest->mHeader.mProtocolID = 0; NewRequest->mHeader.mTransactionID = (qint16)GetNewTransactionID(); NewRequest->mHeader.mUnitID = mDeviceID; mRequestsList.append(NewRequest); SendModbusRequest(NewRequest); NewRequest->mRequestTimer->start(mModbusRequestTimeout); return RET_OK; } void CModbusBackend::RequestTimerExpired() { //find the expired request for(int i = 0; i < mRequestsList.size(); i++) { if(mRequestsList.at(i)->mRequestTimer->isActive() == false) { if(mRequestsList.at(i)->mRetries >= mModbusMaxRetry) { //The max number of retry has been reached. The device is probably offline. CZTLog::instance()->AddLogString("Modbus Maître: Nombre maximal de tentatives sans réponse atteint avec le partenaire",true); delete mRequestsList[i]; mRequestsList.removeAt(i); //TODO: Manage this situation (log?) return; } else { SendModbusRequest(mRequestsList[i]); mRequestsList.at(i)->mRequestTimer->start(mModbusRequestTimeout); mRequestsList[i]->mRetries++; } } } } quint16 CModbusBackend::GetNewTransactionID() { quint16 ID = mTransactionIDCounter++; if(mTransactionIDCounter == 0xFFFF - 10) { mTransactionIDCounter = 0; } return ID; } CModbusRequest::CModbusRequest(): mRetries(0) { mRequestTimer = new QTimer; mRequestTimer->setSingleShot(true); } CModbusRequest::~CModbusRequest() { delete mRequestTimer; } QDataStream &operator<<(QDataStream &out, const CModbusHeader &source) { out << source.mTransactionID << source.mProtocolID << source.mMessageLength << source.mUnitID ; return out; } QDataStream &operator>>(QDataStream &in, CModbusHeader &dest) { in >> dest.mTransactionID >> dest.mProtocolID >> dest.mMessageLength >> dest.mUnitID ; return in; } //Virtual function that should not even get called... void CModbusBackend::ModbusResponseException(quint8 ExceptionCode, quint8 FctCode) { Q_UNUSED(ExceptionCode) Q_UNUSED(FctCode) CEngLog::instance()->AddLogString("ModbusResponseException called from within slave object... weird stuff!"); } //Virtual function that should not even get called... void CModbusBackend::ModbusRequestException(quint8 ExceptionCode, quint8 FctCode) { Q_UNUSED(ExceptionCode) Q_UNUSED(FctCode) CEngLog::instance()->AddLogString("ModbusResponseException called from within master object... weird stuff!"); }