// 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();
}