diff --git a/CMakeLists.txt b/CMakeLists.txt index efe3d85..2d3bea9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,6 @@ add_library( include/aniray/DMXAddr.hpp include/aniray/Geometry.hpp include/aniray/IOInterface.hpp - include/aniray/IOInterfaceModbus.hpp include/aniray/Node.hpp include/aniray/NodeAnimation.hpp include/aniray/NodeArray.hpp @@ -85,7 +84,9 @@ if(NOT ANIRAY_WITHOUT_OLA) endif() if(NOT ANIRAY_WITHOUT_MODBUS) # Private to avoid double linking https://stackoverflow.com/a/34533415 - target_sources(${CMAKE_PROJECT_NAME} PRIVATE include/aniray/IOInterfaceModbus.hpp) + target_sources(${CMAKE_PROJECT_NAME} PRIVATE + include/aniray/IOInterfaceModbus.hpp + src/IOInterfaceModbus.cpp) endif() if(ANIRAY_WITH_LINT) @@ -97,10 +98,6 @@ if(ANIRAY_WITH_LINT) src/lintHelpers/NodeArray.cpp src/lintHelpers/NodeArrayOutput.cpp src/lintHelpers/NodeArraySampler.cpp) - if(NOT ANIRAY_WITHOUT_MODBUS) - # Private to avoid double linking https://stackoverflow.com/a/34533415 - target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/lintHelpers/IOInterfaceModbus.cpp) - endif() set_target_properties( ${CMAKE_PROJECT_NAME} PROPERTIES diff --git a/include/aniray/IOInterface.hpp b/include/aniray/IOInterface.hpp index 2dc1259..353db8f 100644 --- a/include/aniray/IOInterface.hpp +++ b/include/aniray/IOInterface.hpp @@ -61,11 +61,11 @@ class IOInterfaceInput { // https://en.cppreference.com/w/cpp/container/vector_bool using IOInterfaceInputDiscrete = IOInterfaceInput; -class IOInterface { +class IOInterfaceGeneric { public: [[nodiscard]] auto getInputsDiscrete(const std::string &name) const -> std::shared_ptr; - protected: virtual void refreshInputs() = 0; + protected: void assignInputDiscrete(const std::string &name, std::shared_ptr input); private: std::unordered_map> mInputsDiscrete; diff --git a/include/aniray/IOInterfaceModbus.hpp b/include/aniray/IOInterfaceModbus.hpp index d2284b9..6843203 100644 --- a/include/aniray/IOInterfaceModbus.hpp +++ b/include/aniray/IOInterfaceModbus.hpp @@ -38,132 +38,56 @@ namespace aniray { namespace IOInterface { -namespace IOInterfaceModbus { +namespace Modbus { -enum ConfigType { - CONFIG_TYPE_TCP +enum class ConfigFunctionsAddressLayout { + ADDRESS, // each address is an input + BITS_LSB, // each bit is an input, first bit is first address + SPAN_2_LSB // 16- or 32-bit value over two addresses, LSB }; -struct Config { - ConfigType type; - std::string address; - std::uint16_t port; - std::vector functions; -}; -enum ConfigFunctionsType { - CONFIG_FUNCTIONS_TYPE_INPUT_DISCRETE - // CONFIG_FUNCTIONS_TYPE_INPUT_COUNTER -}; -struct ConfigFunction { +struct ConfigInputDiscrete { std::string name; - ConfigFunctionsType type; std::uint8_t slaveID; std::uint8_t functionCode; + ConfigFunctionsAddressLayout addressLayout; std::uint16_t startAddress; - std::uint16_t numItems; -// bool clearCounterEnable; -// std::uint8_t clearCounterFunctionCode; -// std::uint16_t clearCounterAddress; + std::uint16_t numAddressedItems; + bool enableClear; + std::uint8_t clearFunctionCode; + bool clearSingleAddress; + std::uint16_t clearAddress; +// std::uint16_t clearNumAddressedItems; }; +// Not enum to enforce strong typing const std::uint8_t FUNCTION_CODE_READ_BITS = 1; const std::uint8_t FUNCTION_CODE_READ_INPUT_BITS = 2; const std::uint8_t FUNCTION_CODE_READ_REGISTERS = 3; const std::uint8_t FUNCTION_CODE_READ_INPUT_REGISTERS = 4; +const std::uint8_t FUNCTION_CODE_FORCE_SINGLE_COIL = 5; -class IOInterfaceModbus : public aniray::IOInterface::IOInterface { +class IOInterfaceModbus : public aniray::IOInterface::IOInterfaceGeneric { public: - IOInterfaceModbus(Config config) - : mConfig(config) { - switch (mConfig.type) { - case CONFIG_TYPE_TCP: - setupConnectionTCP(); - break; - default: - throw std::runtime_error("IOInterfaceModbus: Unknown connection type!"); - break; - } - - for (ConfigFunction functionConfig : mConfig.functions) { - switch (functionConfig.type) { - case CONFIG_FUNCTIONS_TYPE_INPUT_DISCRETE: - setupInputsDiscrete(functionConfig); - break; - // case CONFIG_FUNCTIONS_TYPE_INPUT_COUNTER: - // setupCounter(functionConfig); - // break; - default: - throw std::runtime_error("IOInterfaceModbus: Unknown function type!"); - break; - } - } - - // Refresh inputs to initially populate values and confirm all addresses are reachable - refreshInputs(); - } - - ~IOInterfaceModbus() { - modbus_close(mCTX); - modbus_free(mCTX); - } + IOInterfaceModbus(std::string tcpAddress, std::uint16_t tcpPort); + ~IOInterfaceModbus(); + void refreshInputs() override; + void setupInputDiscrete(std::string name, + std::uint8_t slaveID, + std::uint8_t functionCode, + ConfigFunctionsAddressLayout addressLayout, + std::uint16_t startAddress, + std::uint16_t numAddressedItems, + bool enableClear = false, + std::uint8_t clearFunctionCode = FUNCTION_CODE_FORCE_SINGLE_COIL, + bool clearSingleAddress = false, + std::uint16_t clearAddress = 0); private: - Config mConfig; + std::unordered_map mInputsDiscreteModbus; + mutable std::shared_mutex mMutexInputsDiscreteModbus; modbus_t *mCTX; - void setupConnectionTCP() { - mCTX = modbus_new_tcp_pi(mConfig.address.c_str(), std::to_string(mConfig.port).c_str()); - if (mCTX == NULL) { - throw std::runtime_error("IOInterfaceModbus: Unable to allocate libmodbus context"); - } else if (modbus_connect(mCTX) == -1) { - modbus_free(mCTX); - throw std::runtime_error("IOInterfaceModbus: Connection failed: " + std::string(modbus_strerror(errno))); - } - BOOST_LOG_TRIVIAL(info) << "IOInterfaceModbus: Connected to " - << mConfig.address << ":" << mConfig.port; - } - - void setupInputsDiscrete(ConfigFunction functionConfig) { - mInputsDiscrete.assignInputDiscrete(functionConfig.name, std::make_shared()); - } - - void refreshInputs() override { - for (ConfigFunction functionConfig : mConfig.functions) { - switch (functionConfig.type) { - case CONFIG_FUNCTIONS_TYPE_INPUT_DISCRETE: - updateInputsDiscrete(functionConfig); - break; - // case CONFIG_FUNCTIONS_TYPE_INPUT_COUNTER: - // updateInputsCounter(functionConfig); - // break; - default: - throw std::runtime_error("IOInterfaceModbus: Unknown function type!"); - break; - } - } - } - - void updateInputsDiscrete(ConfigFunction functionConfig) { - if (modbus_set_slave(mCTX, functionConfig.slaveID) == -1) { - throw std::runtime_error("IOInterfaceModbus: Invalid slave ID: " + std::to_string(functionConfig.slaveID)); - } - switch (functionConfig.functionCode) { - case FUNCTION_CODE_READ_BITS: - std::vector dest(functionConfig.numItems); - modbus_read_bits(mCTX, functionConfig.startAddress, functionConfig.numItems); - mInputsDiscrete[functionConfig.name].setValues(&dest[0]); - break; - - case FUNCTION_CODE_READ_INPUT_BITS: - std::vector dest(functionConfig.numItems); - modbus_read_input_bits(mCTX, functionConfig.startAddress, functionConfig.numItems); - mInputsDiscrete[functionConfig.name].setValues(&dest[0]); - break; - - default: - throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input function code!"); - break; - - } - } + void setupConnectionTCP(std::string tcpAddress, std::uint16_t tcpPort); + void updateInputDiscrete(ConfigInputDiscrete configInputDiscrete); // void updateInputsCounter(ConfigFunction functionConfig) {} }; diff --git a/src/IOInterface.cpp b/src/IOInterface.cpp index e9150f1..08b46a2 100644 --- a/src/IOInterface.cpp +++ b/src/IOInterface.cpp @@ -34,13 +34,13 @@ namespace aniray::IOInterface { -auto IOInterface::getInputsDiscrete(const std::string &name) const -> std::shared_ptr { +auto IOInterfaceGeneric::getInputsDiscrete(const std::string &name) const -> std::shared_ptr { return mInputsDiscrete.at(name); } -void IOInterface::assignInputDiscrete(const std::string &name, std::shared_ptr input) { +void IOInterfaceGeneric::assignInputDiscrete(const std::string &name, std::shared_ptr input) { if (mInputsDiscrete.count(name) > 0) { - throw std::runtime_error("IOInterfaceModbus: Duplicate discrete input! Name: " + name); + throw std::runtime_error("IOInterfaceGeneric: Duplicate discrete input! Name: " + name); } mInputsDiscrete[name] = std::move(input); } diff --git a/src/IOInterfaceModbus.cpp b/src/IOInterfaceModbus.cpp new file mode 100644 index 0000000..6bf0b71 --- /dev/null +++ b/src/IOInterfaceModbus.cpp @@ -0,0 +1,208 @@ +/* IOInterfaceModbus.cpp: Modbus IO for Aniray systems + * + * Created by Perry Naseck on 2023-02-09. + * + * This file is a part of Aniray + * https://github.com/HypersonicED/aniray + * + * Copyright (c) 2023, Hypersonic + * Copyright (c) 2023, Perry Naseck + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace aniray { +namespace IOInterface { +namespace Modbus { + +IOInterfaceModbus::IOInterfaceModbus(std::string tcpAddress, std::uint16_t tcpPort) { + setupConnectionTCP(tcpAddress, tcpPort); + + // Refresh inputs to initially populate values and confirm all addresses are reachable + // refreshInputs(); + } + +IOInterfaceModbus::~IOInterfaceModbus() { + modbus_close(mCTX); + modbus_free(mCTX); +} + +void IOInterfaceModbus::setupConnectionTCP(std::string tcpAddress, std::uint16_t tcpPort) { + mCTX = modbus_new_tcp_pi(tcpAddress.c_str(), std::to_string(tcpPort).c_str()); + if (mCTX == NULL) { + throw std::runtime_error("IOInterfaceModbus: Unable to allocate libmodbus context"); + } else if (modbus_connect(mCTX) == -1) { + modbus_free(mCTX); + throw std::runtime_error("IOInterfaceModbus: Connection failed: " + std::string(modbus_strerror(errno))); + } + BOOST_LOG_TRIVIAL(info) << "IOInterfaceModbus: Connected to " + << tcpAddress << ":" << tcpPort; +} + +void IOInterfaceModbus::setupInputDiscrete(std::string name, + std::uint8_t slaveID, + std::uint8_t functionCode, + ConfigFunctionsAddressLayout addressLayout, + std::uint16_t startAddress, + std::uint16_t numAddressedItems, + bool enableClear, + std::uint8_t clearFunctionCode, + bool clearSingleAddress, + std::uint16_t clearAddress) { + assignInputDiscrete(name, std::make_shared()); + // above call checks for duplicates + const std::unique_lock lock(mMutexInputsDiscreteModbus); + mInputsDiscreteModbus[name] = { + name: name, + slaveID: slaveID, + functionCode: functionCode, + addressLayout: addressLayout, + startAddress: static_cast(startAddress - 1U), // seems to always be off by one (starts at 0?) + numAddressedItems: numAddressedItems, + enableClear: enableClear, + clearFunctionCode: clearFunctionCode, + clearSingleAddress: clearSingleAddress, + clearAddress: clearAddress + }; +} + +void IOInterfaceModbus::refreshInputs() { + const std::shared_lock inputsDiscreteLock(mMutexInputsDiscreteModbus); + for (auto const& [name, configInputDiscrete] : mInputsDiscreteModbus) { + updateInputDiscrete(configInputDiscrete); + } + // inputsDiscreteLock.unlock(); // will need this when other types of input are added +} + +void IOInterfaceModbus::updateInputDiscrete(ConfigInputDiscrete configInputDiscrete) { + if (modbus_set_slave(mCTX, configInputDiscrete.slaveID) == -1) { + throw std::runtime_error("IOInterfaceModbus: Invalid slave ID: " + std::to_string(configInputDiscrete.slaveID)); + } + std::vector dest8(configInputDiscrete.numAddressedItems); + std::vector dest16(configInputDiscrete.numAddressedItems); + switch (configInputDiscrete.functionCode) { + case FUNCTION_CODE_READ_BITS: + modbus_read_bits(mCTX, configInputDiscrete.startAddress, configInputDiscrete.numAddressedItems, &dest8[0]); + break; + + case FUNCTION_CODE_READ_INPUT_BITS: + modbus_read_input_bits(mCTX, configInputDiscrete.startAddress, configInputDiscrete.numAddressedItems, &dest8[0]); + break; + + case FUNCTION_CODE_READ_REGISTERS: + modbus_read_registers(mCTX, configInputDiscrete.startAddress, configInputDiscrete.numAddressedItems, &dest16[0]); + break; + + case FUNCTION_CODE_READ_INPUT_REGISTERS: + modbus_read_input_registers(mCTX, configInputDiscrete.startAddress, configInputDiscrete.numAddressedItems, &dest16[0]); + break; + + default: + throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input function code!"); + break; + + } + std::vector out; + switch (configInputDiscrete.addressLayout) { + case ConfigFunctionsAddressLayout::ADDRESS: + switch (configInputDiscrete.functionCode) { + case FUNCTION_CODE_READ_BITS: + case FUNCTION_CODE_READ_INPUT_BITS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems; i++) { + out.push_back(static_cast(dest8[i])); + } + break; + case FUNCTION_CODE_READ_REGISTERS: + case FUNCTION_CODE_READ_INPUT_REGISTERS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems; i++) { + out.push_back(static_cast(dest16[i])); + } + break; + default: + throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input function code!"); + break; + } + break; + case ConfigFunctionsAddressLayout::BITS_LSB: + switch (configInputDiscrete.functionCode) { + case FUNCTION_CODE_READ_BITS: + case FUNCTION_CODE_READ_INPUT_BITS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems; i++) { + for (std::size_t i = 0; i < 8; i++) { + auto val = (dest8[i] >> i) & 1; + out.push_back(static_cast(val)); + } + } + break; + case FUNCTION_CODE_READ_REGISTERS: + case FUNCTION_CODE_READ_INPUT_REGISTERS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems; i++) { + for (std::size_t i = 0; i < 16; i++) { + auto val = (dest16[i] >> i) & 1; + out.push_back(static_cast(val)); + } + } + break; + default: + throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input function code!"); + break; + } + break; + case ConfigFunctionsAddressLayout::SPAN_2_LSB: + // For discrete we use only the first bit, so keep it simple and skip higher bit addresses + switch (configInputDiscrete.functionCode) { + case FUNCTION_CODE_READ_BITS: + case FUNCTION_CODE_READ_INPUT_BITS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems / 2; i += 2) { + out.push_back(static_cast(dest8[i])); + } + break; + case FUNCTION_CODE_READ_REGISTERS: + case FUNCTION_CODE_READ_INPUT_REGISTERS: + for (std::size_t i = 0; i < configInputDiscrete.numAddressedItems / 2; i += 2) { + out.push_back(static_cast(dest16[i])); + } + break; + default: + throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input function code!"); + break; + } + break; + + default: + throw std::runtime_error("IOInterfaceModbus: Incorrect discrete input address layout!"); + break; + } + std::shared_ptr values = getInputsDiscrete(configInputDiscrete.name); + values->setValues(out); +} + +} // namespace Modbus +} // namespace IOInterface +} // namespace aniray diff --git a/src/lintHelpers/IOInterfaceModbus.cpp b/src/lintHelpers/IOInterfaceModbus.cpp deleted file mode 100644 index 79d7af7..0000000 --- a/src/lintHelpers/IOInterfaceModbus.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/* InputInterfaceModbus.cpp: Source file to activate linters. - * - * Created by Perry Naseck on 2022-11-03. - * - * This file is a part of Aniray - * https://github.com/HypersonicED/aniray - * - * Copyright (c) 2022, Hypersonic - * Copyright (c) 2022, Perry Naseck - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include