// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins-modbus. * * nymea-plugins-modbus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins-modbus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-plugins-modbus. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "neuroncommon.h" #include "extern-plugininfo.h" NeuronCommon::NeuronCommon(QModbusClient *modbusInterface, int slaveAddress, QObject *parent) : QObject(parent), m_slaveAddress(slaveAddress), m_modbusInterface(modbusInterface) { m_inputPollingTimer = new QTimer(this); connect(m_inputPollingTimer, &QTimer::timeout, this, &NeuronCommon::onInputPollingTimer); m_inputPollingTimer->setTimerType(Qt::TimerType::PreciseTimer); m_inputPollingTimer->setInterval(200); m_outputPollingTimer = new QTimer(this); connect(m_outputPollingTimer, &QTimer::timeout, this, &NeuronCommon::onOutputPollingTimer); m_outputPollingTimer->setTimerType(Qt::TimerType::PreciseTimer); m_outputPollingTimer->setInterval(1000); if (m_modbusInterface->state() == QModbusDevice::State::ConnectedState) { m_inputPollingTimer->start(); m_outputPollingTimer->start(); } connect(m_modbusInterface, &QModbusDevice::stateChanged, this, [this] (QModbusDevice::State state) { if (state == QModbusDevice::State::ConnectedState) { if (m_inputPollingTimer) m_inputPollingTimer->start(); if (m_outputPollingTimer) m_outputPollingTimer->start(); emit connectionStateChanged(true); } else { if (m_inputPollingTimer) m_inputPollingTimer->stop(); if (m_outputPollingTimer) m_outputPollingTimer->stop(); emit connectionStateChanged(false); } }); } bool NeuronCommon::init() { qCDebug(dcUniPi()) << "Neuron: Init"; if (!loadModbusMap()) { return false; } if (!m_modbusInterface) { qWarning(dcUniPi()) << "Neuron: Modbus interface not available"; return false; } if (m_modbusInterface->connectDevice()) { qWarning(dcUniPi()) << "Neuron: Could not connect to modbus device"; return false; } return true; } int NeuronCommon::slaveAddress() { return m_slaveAddress; } void NeuronCommon::setSlaveAddress(int slaveAddress) { qCDebug(dcUniPi()) << "Neuron: Set slave address" << slaveAddress; m_slaveAddress = slaveAddress; } QList NeuronCommon::digitalInputs() { return m_modbusDigitalInputRegisters.keys(); } QList NeuronCommon::digitalOutputs() { return m_modbusDigitalOutputRegisters.keys(); } QList NeuronCommon::analogInputs() { QList circuits; Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogInputRegisters.values()) { circuits.append(descriptor.circuit); } return circuits; } QList NeuronCommon::analogOutputs() { QList circuits; Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) { circuits.append(descriptor.circuit); } return circuits; } QList NeuronCommon::userLEDs() { return m_modbusUserLEDRegisters.keys(); } NeuronCommon::RegisterDescriptor NeuronCommon::registerDescriptorFromStringList(const QStringList &data) { RegisterDescriptor descriptor; if (data.length() < 7) { return descriptor; } descriptor.address = data[0].toInt(); descriptor.count = data[2].toInt(); if (data[3] == "RW") { descriptor.readWrite = RWPermissionReadWrite; } else if (data[3] == "W") { descriptor.readWrite = RWPermissionWrite; } else if (data[3] == "R") { descriptor.readWrite = RWPermissionRead; } descriptor.circuit = data[5].split(" ").last(); descriptor.category = data.last(); if (data[5].contains("Analog Input Value", Qt::CaseSensitivity::CaseInsensitive)) { descriptor.registerType = QModbusDataUnit::RegisterType::InputRegisters; } else if (data[5].contains("Analog Output Value", Qt::CaseSensitivity::CaseInsensitive)) { descriptor.registerType = QModbusDataUnit::RegisterType::HoldingRegisters; } return descriptor; } bool NeuronCommon::circuitValueChanged(const QString &circuit, quint32 value) { if (m_previousCircuitValue.contains(circuit)) { if (m_previousCircuitValue.value(circuit) == value) { // Only emit status change of the circuit value has changed return false; } else { m_previousCircuitValue.insert(circuit, value); //update existing value return true; } } else { m_previousCircuitValue.insert(circuit, value); return true; } } void NeuronCommon::getAllDigitalInputs() { getCoils(m_modbusDigitalInputRegisters.values()); } void NeuronCommon::getAllDigitalOutputs() { getCoils(m_modbusDigitalOutputRegisters.values()); } void NeuronCommon::getAllAnalogInputs() { Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogInputRegisters.values()) { getAnalogIO(descriptor); } } void NeuronCommon::getAllAnalogOutputs() { Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) { getAnalogIO(descriptor); } } bool NeuronCommon::getDigitalInput(const QString &circuit) { if (!m_modbusDigitalInputRegisters.contains(circuit)) { qCWarning(dcUniPi()) << "Neuron: Digital input circuit not found" << circuit; return ""; } int modbusAddress = m_modbusDigitalInputRegisters.value(circuit); //qDebug(dcUniPi()) << "Neuron: Reading digital Input" << circuit << modbusAddress; QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1); if (m_readRequestQueue.isEmpty()) { return modbusReadRequest(request); } else if (m_readRequestQueue.length() > 100) { qCWarning(dcUniPi()) << "Neuron: Too many pending read requests"; return false; } else { m_readRequestQueue.append(request); } return true; } bool NeuronCommon::getAnalogOutput(const QString &circuit) { //qDebug(dcUniPi()) << "Neuron: Get analog output" << circuit; Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) { if (descriptor.circuit == circuit) { return getAnalogIO(descriptor); } } qCWarning(dcUniPi()) << "Neuron: Analog output circuit not found" << circuit; return false; } QUuid NeuronCommon::setDigitalOutput(const QString &circuit, bool value) { if (!m_modbusDigitalOutputRegisters.contains(circuit)) { qCWarning(dcUniPi()) << "Neuron: Digital output circuit not found" << circuit; return QUuid(); } int modbusAddress = m_modbusDigitalOutputRegisters.value(circuit); //qDebug(dcUniPi()) << "Neuron: Setting digital ouput" << circuit << modbusAddress << value; Request request; request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1); request.data.setValue(0, static_cast(value)); request.id = QUuid::createUuid(); if (m_writeRequestQueue.isEmpty()) { modbusWriteRequest(request); } else if (m_writeRequestQueue.length() > 100) { return QUuid(); } else { m_writeRequestQueue.append(request); } return request.id; } bool NeuronCommon::getDigitalOutput(const QString &circuit) { if (!m_modbusDigitalOutputRegisters.contains(circuit)) { qCWarning(dcUniPi()) << "Neuron: Digital output circuit not found" << circuit; return false; } int modbusAddress = m_modbusDigitalOutputRegisters.value(circuit); //qDebug(dcUniPi()) << "Reading digital Output" << circuit << modbusAddress; QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, modbusAddress, 1); if (m_readRequestQueue.isEmpty()) { return modbusReadRequest(request); } else if (m_readRequestQueue.length() > 100) { qCWarning(dcUniPi()) << "Neuron: Too many pending read requests"; return false; } else { m_readRequestQueue.append(request); } return true; } QUuid NeuronCommon::setAnalogOutput(const QString &circuit, double value) { qDebug(dcUniPi()) << "Neuron: Set analog output" << circuit << value; Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters) { if (descriptor.circuit == circuit) { Request request; request.id = QUuid::createUuid(); request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, descriptor.address, descriptor.count); if (descriptor.count == 1) { request.data.setValue(0, (static_cast(value*400))); // 0 to 4000 = 0 to 10.0 V } else if (descriptor.count == 2) { request.data.setValue(0, (static_cast(value) >> 16)); request.data.setValue(1, (static_cast(value) & 0xffff)); } if (m_writeRequestQueue.isEmpty()) { modbusWriteRequest(request); } else if (m_writeRequestQueue.length() > 100) { return QUuid(); } else { m_writeRequestQueue.append(request); } return request.id; } } qCWarning(dcUniPi()) << "Neuron: Analog output circuit not found" << circuit; return QUuid(); } bool NeuronCommon::getAnalogInput(const QString &circuit) { //qDebug(dcUniPi()) << "Neuron: Get analog input" << circuit; Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) { if (descriptor.circuit == circuit) { return getAnalogIO(descriptor); } } return false; } QUuid NeuronCommon::setUserLED(const QString &circuit, bool value) { int modbusAddress = m_modbusUserLEDRegisters.value(circuit); //qDebug(dcUniPi()) << "Neuron: Setting user led" << circuit << modbusAddress << value; if (!m_modbusInterface) return QUuid(); Request request; request.id = QUuid::createUuid(); request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1); request.data.setValue(0, static_cast(value)); if (m_writeRequestQueue.isEmpty()) { if (!modbusWriteRequest(request)) { return QUuid(); } } else if (m_writeRequestQueue.length() > 100) { return QUuid(); } else { m_writeRequestQueue.append(request); } return request.id; } bool NeuronCommon::getUserLED(const QString &circuit) { int modbusAddress = m_modbusUserLEDRegisters.value(circuit); //qDebug(dcUniPi()) << "Neuron: Get user LED" << circuit << modbusAddress; QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1); if (m_readRequestQueue.isEmpty()) { return modbusReadRequest(request); } else if (m_readRequestQueue.length() > 100) { qCWarning(dcUniPi()) << "Neuron: Too many pending read requests"; return false; } else { m_readRequestQueue.append(request); } return true; } bool NeuronCommon::getAnalogIO(const RegisterDescriptor &descriptor) { if (!m_modbusInterface) return false; if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState) return false; QModbusDataUnit request = QModbusDataUnit(descriptor.registerType, descriptor.address, descriptor.count); if (m_readRequestQueue.isEmpty()) { return modbusReadRequest(request); } else if (m_readRequestQueue.length() > 100) { qCWarning(dcUniPi()) << "Neuron: Too many pending read requests"; return false; } else { m_readRequestQueue.append(request); } return true; } bool NeuronCommon::modbusWriteRequest(const Request &request) { if (!m_modbusInterface) { emit requestExecuted(request.id, false); emit requestError(request.id, "Modbus interface not available"); return false; } if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState) { emit requestExecuted(request.id, false); emit requestError(request.id, "Device not connected"); return false; }; if (QModbusReply *reply = m_modbusInterface->sendWriteRequest(request.data, m_slaveAddress)) { if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); connect(reply, &QModbusReply::finished, this, [reply, request, this] { if (!m_writeRequestQueue.isEmpty()) { modbusWriteRequest(m_writeRequestQueue.takeFirst()); } if (reply->error() == QModbusDevice::NoError) { emit requestExecuted(request.id, true); const QModbusDataUnit unit = reply->result(); int modbusAddress = unit.startAddress(); if(m_modbusDigitalOutputRegisters.values().contains(modbusAddress)){ QString circuit = m_modbusDigitalOutputRegisters.key(modbusAddress); emit digitalOutputStatusChanged(circuit, unit.value(0)); } else if(m_modbusAnalogOutputRegisters.contains(modbusAddress)){ QString circuit = m_modbusAnalogOutputRegisters.value(modbusAddress).circuit; emit analogOutputStatusChanged(circuit, unit.value(0)); } else if(m_modbusUserLEDRegisters.values().contains(modbusAddress)){ QString circuit = m_modbusUserLEDRegisters.key(modbusAddress); emit userLEDStatusChanged(circuit, unit.value(0)); } } else { emit requestExecuted(request.id, false); qCWarning(dcUniPi()) << "Neuron: Write response error:" << reply->error(); emit requestError(request.id, reply->errorString()); } }); QTimer::singleShot(m_responseTimeoutTime, reply, &QModbusReply::deleteLater); } else { reply->deleteLater(); // broadcast replies return immediately return false; } } else { qCWarning(dcUniPi()) << "Neuron: Read error: " << m_modbusInterface->errorString(); return false; } return true; } bool NeuronCommon::modbusReadRequest(const QModbusDataUnit &request) { if (!m_modbusInterface) { return false; } if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState) return false; if (QModbusReply *reply = m_modbusInterface->sendReadRequest(request, m_slaveAddress)) { if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); connect(reply, &QModbusReply::finished, this, [reply, this] { int modbusAddress = 0; if (reply->error() == QModbusDevice::NoError) { const QModbusDataUnit unit = reply->result(); for (uint i = 0; i < unit.valueCount(); i++) { //qCDebug(dcUniPi()) << "Start Address:" << unit.startAddress() << "Register Type:" << unit.registerType() << "Value:" << unit.value(i); modbusAddress = unit.startAddress() + i; QString circuit; switch (unit.registerType()) { case QModbusDataUnit::RegisterType::Coils: if(m_modbusDigitalInputRegisters.values().contains(modbusAddress)){ circuit = m_modbusDigitalInputRegisters.key(modbusAddress); if (circuitValueChanged(circuit, unit.value(i))) emit digitalInputStatusChanged(circuit, unit.value(i)); } else if(m_modbusDigitalOutputRegisters.values().contains(modbusAddress)){ circuit = m_modbusDigitalOutputRegisters.key(modbusAddress); if (circuitValueChanged(circuit, unit.value(i))) emit digitalOutputStatusChanged(circuit, unit.value(i)); } else if(m_modbusUserLEDRegisters.values().contains(modbusAddress)){ circuit = m_modbusUserLEDRegisters.key(modbusAddress); if (circuitValueChanged(circuit, unit.value(i))) emit userLEDStatusChanged(circuit, unit.value(i)); } else { qCWarning(dcUniPi()) << "Neuron: Received unrecognised coil register" << modbusAddress; } break; case QModbusDataUnit::RegisterType::HoldingRegisters: { if (m_modbusAnalogOutputRegisters.keys().contains(modbusAddress)) { RegisterDescriptor descriptor = m_modbusAnalogOutputRegisters.value(modbusAddress); circuit = descriptor.circuit; quint32 value = 0; if (descriptor.count == 1) { value = unit.value(i); } else if (descriptor.count == 2) { if (unit.valueCount() >= (i+1)) { value = (unit.value(i) << 16 | unit.value(i+1)); i++; } else { qCWarning(dcUniPi()) << "Neuron: Received analog output, but value count is too short"; } } if (circuitValueChanged(circuit, value)) emit analogOutputStatusChanged(circuit, value); } else { qCWarning(dcUniPi()) << "Neuron: Received unrecognised holding register" << modbusAddress; } } break; case QModbusDataUnit::RegisterType::InputRegisters: if(m_modbusAnalogInputRegisters.keys().contains(modbusAddress)){ RegisterDescriptor descriptor = m_modbusAnalogInputRegisters.value(modbusAddress); circuit = descriptor.circuit; quint32 value = 0; if (descriptor.count == 1) { value = unit.value(i); } else if (descriptor.count == 2) { if (unit.valueCount() >= (i+1)) { value = (unit.value(i) << 16 | unit.value(i+1)); i++; } else { qCWarning(dcUniPi()) << "Neuron: Received analog input, but value count is too short"; } } if (circuitValueChanged(circuit, value)) emit analogInputStatusChanged(circuit, value); } else { qCWarning(dcUniPi()) << "Neuron: Received unrecognised input register" << modbusAddress; } break; case QModbusDataUnit::RegisterType::DiscreteInputs: case QModbusDataUnit::RegisterType::Invalid: qCWarning(dcUniPi()) << "Neuron: Invalide register type"; break; } } } else if (reply->error() == QModbusDevice::ProtocolError) { qCWarning(dcUniPi()) << "Neuron: Read response error:" << reply->errorString() << reply->rawResult().exceptionCode(); } else { qCWarning(dcUniPi()) << "Neuron: Read response error:" << reply->error() << reply->errorString(); } }); QTimer::singleShot(m_responseTimeoutTime, reply, &QModbusReply::deleteLater); } else { reply->deleteLater(); // broadcast replies return immediately return false; } } else { qCWarning(dcUniPi()) << "Neuron: Read error: " << m_modbusInterface->errorString(); return false; } return true; } void NeuronCommon::getCoils(QList registerList) { if (registerList.isEmpty()) { return; } std::sort(registerList.begin(), registerList.end()); int previousReg = registerList.first(); //first register to read and starting point to get the following registers int startAddress; QHash registerGroups; foreach (int reg, registerList) { //qDebug(dcUniPi()) << "Register" << reg << "previous Register" << previousReg; if (reg == previousReg) { //first register startAddress = reg; registerGroups.insert(startAddress, 1); } else if (reg == (previousReg + 1)) { //next register in block previousReg = reg; registerGroups.insert(startAddress, (registerGroups.value(startAddress) + 1)); //update block length } else { // new block startAddress = reg; previousReg = reg; registerGroups.insert(startAddress, 1); } } foreach (int startAddress, registerGroups.keys()) { QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, startAddress, registerGroups.value(startAddress)); if (m_readRequestQueue.isEmpty()) { modbusReadRequest(request); } else if (m_readRequestQueue.length() > 100) { qCWarning(dcUniPi()) << "Neuron: Too many pending read requests"; } else { m_readRequestQueue.append(request); } } } void NeuronCommon::onOutputPollingTimer() { getAllDigitalOutputs(); getAllAnalogOutputs(); } void NeuronCommon::onInputPollingTimer() { getAllDigitalInputs(); getAllAnalogInputs(); }