Merge pull request #118 from garretfick/feature/INTEGRATION_TEST
Integration tests
This commit is contained in:
commit
c2b9e3b3a1
|
@ -41,6 +41,11 @@ if(NOT program_name)
|
|||
endif()
|
||||
message("User program = ${program_name}")
|
||||
|
||||
if(NOT USER_STSOURCE_DIR)
|
||||
set(USER_STSOURCE_DIR "../etc/st_files")
|
||||
endif()
|
||||
message("User program directory = ${USER_STSOURCE_DIR}")
|
||||
|
||||
# Enable building the application with different set of capabilties
|
||||
# depending on the capabilities that we want.
|
||||
|
||||
|
@ -200,8 +205,8 @@ if(OPLC_ST_TO_C)
|
|||
# Dummy output rebuilded everytime. Needed for Config0.c and Res0.c files generation
|
||||
add_custom_command(OUTPUT dummy_output
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ../etc/src
|
||||
COMMAND ./st_optimizer${PLATFORM_EXTENSION} ../etc/st_files/${program_name} ../etc/st_files/${program_name}
|
||||
COMMAND ./iec2c${PLATFORM_EXTENSION} -I ../runtime/lib -T ../etc/src ../etc/st_files/${program_name}
|
||||
COMMAND ./st_optimizer${PLATFORM_EXTENSION} ${USER_STSOURCE_DIR}/${program_name} ${USER_STSOURCE_DIR}/${program_name}
|
||||
COMMAND ./iec2c${PLATFORM_EXTENSION} -I ../runtime/lib -T ../etc/src ${USER_STSOURCE_DIR}/${program_name}
|
||||
COMMAND ./glue_generator${PLATFORM_EXTENSION} ../etc/src/LOCATED_VARIABLES.h ../etc/src/glueVars.cpp
|
||||
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/bin
|
||||
DEPENDS always_rebuild dependent_tools
|
||||
|
|
|
@ -76,5 +76,5 @@ The normal approach for running OpenPLC is though the web interface. However,
|
|||
it is possible to run OpenPLC without the web interface using configuration
|
||||
information supplied in a configuration file.
|
||||
|
||||
See the file `config.ini.example` in this repository for information about
|
||||
See the file `config.example.ini` in this repository for information about
|
||||
how to run standalone.
|
||||
|
|
|
@ -100,9 +100,10 @@ void bootstrap()
|
|||
PlcConfig config;
|
||||
|
||||
// Try to read the config file
|
||||
if (ini_parse(oplc::config_file, config_handler, &config) < 0)
|
||||
const char* config_path = oplc::get_config_path();
|
||||
if (ini_parse(config_path, config_handler, &config) < 0)
|
||||
{
|
||||
spdlog::info("Config file {} could not be read", oplc::config_file);
|
||||
spdlog::info("Config file {} could not be read", config_path);
|
||||
// If we don't have the config file, then default to always
|
||||
// starting the interactive server.
|
||||
config.services.push_back("interactive");
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2019 Smarter Grid Solutions
|
||||
//
|
||||
// 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 permissionsand
|
||||
// limitations under the License.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include "ini_util.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace oplc
|
||||
{
|
||||
|
||||
const size_t CONFIG_BUFFER_SIZE = 2048;
|
||||
char ini_path[CONFIG_BUFFER_SIZE] = "../etc/config.ini";
|
||||
|
||||
void set_config_path(const char* new_path, size_t count)
|
||||
{
|
||||
spdlog::info("Configuration path set to {}", new_path);
|
||||
strncpy(ini_path, new_path, std::min(count, CONFIG_BUFFER_SIZE));
|
||||
}
|
||||
|
||||
const char* get_config_path()
|
||||
{
|
||||
return ini_path;
|
||||
}
|
||||
|
||||
config_stream open_config()
|
||||
{
|
||||
return config_stream(
|
||||
new std::ifstream(oplc::get_config_path()),
|
||||
[] (std::istream* s)
|
||||
{
|
||||
reinterpret_cast<std::ifstream*>(s)->close();
|
||||
delete s;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace oplc
|
|
@ -28,10 +28,22 @@
|
|||
namespace oplc
|
||||
{
|
||||
|
||||
/// Define location of config file once so others can use it
|
||||
static const char* config_file = "../etc/config.ini";
|
||||
/// @brief Gets the path to the INI file.
|
||||
const char* get_config_path();
|
||||
|
||||
/// Convert a boolean value in the INI file to a boolean.
|
||||
/// @brief Sets the path to the configuration INI file. This allows
|
||||
/// the runtime to use an alternative configuration file, for example by
|
||||
/// specifying the alternative through a command line argument.
|
||||
///
|
||||
/// @note This function is not thread safe and it is not safe to call this
|
||||
/// function where other thread may call this or any function that gets
|
||||
/// the ini path.
|
||||
///
|
||||
/// @param new_path The new path to use
|
||||
/// @param count The number of characters in new_path
|
||||
void set_config_path(const char* new_path, std::size_t count);
|
||||
|
||||
/// @brief Convert a boolean value in the INI file to a boolean.
|
||||
/// The value must be "true", otherwise it is interpreted as false.
|
||||
/// @param value the value to convert.
|
||||
/// @return The converted value.
|
||||
|
@ -40,7 +52,7 @@ inline bool ini_atob(const char* value)
|
|||
return strcmp("true", value) == 0;
|
||||
}
|
||||
|
||||
/// Is the section and value equal to the expected section and value?
|
||||
/// @brief Is the section and value equal to the expected section and value?
|
||||
/// @param section_expected The expected section.
|
||||
/// @param value_expected The expected value.
|
||||
/// @param section The current section.
|
||||
|
@ -122,17 +134,7 @@ typedef std::unique_ptr<std::istream, std::function<void(std::istream*)>> config
|
|||
|
||||
/// Open the standard configuration file as an closable stream.
|
||||
/// @return A stream for the configuration file.
|
||||
inline config_stream open_config()
|
||||
{
|
||||
return config_stream(
|
||||
new std::ifstream(config_file),
|
||||
[] (std::istream* s)
|
||||
{
|
||||
reinterpret_cast<std::ifstream*>(s)->close();
|
||||
delete s;
|
||||
}
|
||||
);
|
||||
}
|
||||
config_stream open_config();
|
||||
|
||||
} // namespace oplc
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "iec_types.h"
|
||||
#include "ini_util.h"
|
||||
#include "ladder.h"
|
||||
#include "service/service_definition.h"
|
||||
#include "service/service_registry.h"
|
||||
|
@ -146,9 +147,25 @@ void handleSpecialFunctions()
|
|||
// Insert other special functions below
|
||||
}
|
||||
|
||||
/// Handle the command line arguments by setting things as appropriate.
|
||||
void handle_args(int argc, char** argv)
|
||||
{
|
||||
for (auto i = 0; i < argc; ++i)
|
||||
{
|
||||
if (strcmp(argv[i], "--config") == 0 && i + 1 < argc)
|
||||
{
|
||||
// The next argument is interpreted as the path to
|
||||
// the configuration file
|
||||
oplc::set_config_path(argv[i + 1], strlen(argv[i + 1]));
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
initialize_logging(argc, argv);
|
||||
handle_args(argc, argv);
|
||||
spdlog::info("OpenPLC Runtime starting...");
|
||||
|
||||
time(&start_time);
|
||||
|
@ -166,9 +183,9 @@ int main(int argc, char **argv)
|
|||
//======================================================
|
||||
// MAIN LOOP
|
||||
//======================================================
|
||||
spdlog::trace("Beginning main loop");
|
||||
while (run_openplc)
|
||||
{
|
||||
|
||||
// Read input image - this method tries to get the lock
|
||||
// so don't put it in the lock context.
|
||||
updateBuffersIn();
|
||||
|
@ -178,9 +195,6 @@ int main(int argc, char **argv)
|
|||
// attached to the user variables
|
||||
glueVars();
|
||||
|
||||
// Read input image
|
||||
updateBuffersIn();
|
||||
|
||||
updateCustomIn();
|
||||
// Update input image table with data from slave devices
|
||||
services_before_cycle();
|
||||
|
|
|
@ -73,7 +73,7 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
|
|||
lock_guard<mutex> guard(*this->glue_mutex);
|
||||
// This constructor is pretty long - what we are doing here is
|
||||
// setting up structures that map between caches and the bound
|
||||
// glue. These structures give fast (index -based) read/write of
|
||||
// glue. These structures give fast (index-based) read/write of
|
||||
// values. The caches ensure that we are unlikely to have to wait
|
||||
// for a lock.
|
||||
|
||||
|
@ -125,22 +125,22 @@ IndexedStrategy::IndexedStrategy(const GlueVariablesBinding& bindings) :
|
|||
{
|
||||
int_register_read_buffer[msi].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
}
|
||||
else if (dir == IECLDT_MEM && msi >= MIN_16B_RANGE && msi < MAX_16B_RANGE)
|
||||
else if (dir == IECLDT_MEM && msi < intm_register_read_buffer.size())
|
||||
{
|
||||
intm_register_read_buffer[msi - MIN_16B_RANGE].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
intm_register_read_buffer[msi].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
}
|
||||
else if (dir == IECLDT_IN)
|
||||
{
|
||||
int_input_read_buffer[msi].init(reinterpret_cast<IEC_INT*>(glue_variables[index].value));
|
||||
}
|
||||
}
|
||||
else if (type == IECVT_DINT && dir == IECLDT_MEM && msi <= MIN_32B_RANGE && msi < MAX_32B_RANGE)
|
||||
else if (type == IECVT_DINT && dir == IECLDT_MEM && msi < dintm_register_read_buffer.size())
|
||||
{
|
||||
dintm_register_read_buffer[msi - MIN_32B_RANGE].init(reinterpret_cast<IEC_DINT*>(glue_variables[index].value));
|
||||
dintm_register_read_buffer[msi].init(reinterpret_cast<IEC_DINT*>(glue_variables[index].value));
|
||||
}
|
||||
else if (type == IECVT_LINT && dir == IECLDT_MEM && msi <= MIN_64B_RANGE && msi < MAX_64B_RANGE)
|
||||
{
|
||||
lintm_register_read_buffer[msi - MIN_64B_RANGE].init(reinterpret_cast<IEC_LINT*>(glue_variables[index].value));
|
||||
lintm_register_read_buffer[msi].init(reinterpret_cast<IEC_LINT*>(glue_variables[index].value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,13 +232,8 @@ modbus_errno IndexedStrategy::WriteMultipleCoils(uint16_t coil_start_index,
|
|||
uint16_t num_coils,
|
||||
uint8_t* values)
|
||||
{
|
||||
if (coil_start_index + num_coils >= coil_write_buffer.size())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
lock_guard<mutex> guard(buffer_mutex);
|
||||
for (uint16_t index = 0; index < num_coils; ++index)
|
||||
for (uint16_t index = 0; index < num_coils && (coil_start_index + index) < coil_write_buffer.size(); ++index)
|
||||
{
|
||||
// Get the value from the packed structure
|
||||
bool value = values[index / 8] & BOOL_BIT_MASK[index % 8];
|
||||
|
@ -267,7 +262,7 @@ modbus_errno IndexedStrategy::ReadBools(const vector<MappedBool>& buffer,
|
|||
uint16_t num_values,
|
||||
uint8_t* values)
|
||||
{
|
||||
auto max_index = start_index + num_values - 1;
|
||||
auto max_index = start_index + num_values - 1;
|
||||
if (max_index >= buffer.size())
|
||||
{
|
||||
return -1;
|
||||
|
@ -314,7 +309,7 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
// The word we got is part of a larger 32-bit value, and we will
|
||||
// bit shift to write the appropriate part. Resize to 32-bits
|
||||
// so we can shift appropriately.
|
||||
uint32_t partial_value = (uint32_t) word;
|
||||
uint32_t partial_value = static_cast<uint32_t>(word);
|
||||
oplc::PendingValue<IEC_DINT>& dst = dintm_register_write_buffer[hr_index / 2];
|
||||
dst.has_pending = true;
|
||||
|
||||
|
@ -322,7 +317,7 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
{
|
||||
// First word
|
||||
dst.value = dst.value & 0x0000ffff;
|
||||
dst.value = dst.value | partial_value;
|
||||
dst.value = dst.value | (partial_value << 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -336,7 +331,7 @@ modbus_errno IndexedStrategy::WriteHoldingRegisters(uint16_t hr_start_index,
|
|||
hr_index -= MIN_64B_RANGE;
|
||||
// Same as with a 32-bit value, here we are updating part of a
|
||||
// 64-bit value, so resize so we can bit-shift appropriately.
|
||||
uint64_t partial_value = (uint64_t) word;
|
||||
uint64_t partial_value = static_cast<uint64_t>(word);
|
||||
oplc::PendingValue<IEC_LINT>& dst = lintm_register_write_buffer[hr_index / 4];
|
||||
dst.has_pending = true;
|
||||
|
||||
|
@ -397,11 +392,11 @@ modbus_errno IndexedStrategy::ReadHoldingRegisters(uint16_t hr_start_index, uint
|
|||
hr_index -= MIN_32B_RANGE;
|
||||
if (hr_index % 2 == 0)
|
||||
{
|
||||
val = (uint16_t)(dintm_register_read_buffer[hr_index / 2].cached_value >> 16);
|
||||
val = static_cast<uint16_t>(dintm_register_read_buffer[hr_index / 2].cached_value >> 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
val = (uint16_t)(dintm_register_read_buffer[hr_index / 2].cached_value & 0xffff);
|
||||
val = static_cast<uint16_t>(dintm_register_read_buffer[hr_index / 2].cached_value & 0x0000ffff);
|
||||
}
|
||||
}
|
||||
else if (hr_index < MAX_64B_RANGE)
|
||||
|
@ -409,19 +404,19 @@ modbus_errno IndexedStrategy::ReadHoldingRegisters(uint16_t hr_start_index, uint
|
|||
hr_index -= MIN_64B_RANGE;
|
||||
if (hr_index %4 == 0)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 48);
|
||||
val = static_cast<uint16_t>(lintm_register_read_buffer[hr_index / 4].cached_value >> 48);
|
||||
}
|
||||
else if (hr_index %4 == 1)
|
||||
else if (hr_index % 4 == 1)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 32);
|
||||
val = static_cast<uint16_t>(lintm_register_read_buffer[hr_index / 4].cached_value >> 32);
|
||||
}
|
||||
else if (hr_index %4 == 2)
|
||||
else if (hr_index % 4 == 2)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value >> 16);
|
||||
val = static_cast<uint16_t>(lintm_register_read_buffer[hr_index / 4].cached_value >> 16);
|
||||
}
|
||||
else if (hr_index %4 == 3)
|
||||
else if (hr_index % 4 == 3)
|
||||
{
|
||||
val = (uint16_t)(lintm_register_read_buffer[hr_index / 4].cached_value & 0xffff);
|
||||
val = static_cast<uint16_t>(lintm_register_read_buffer[hr_index / 4].cached_value & 0xffff);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
#define MB_FC_READ_HOLDING_REGISTERS 3
|
||||
#define MB_FC_READ_INPUT_REGISTERS 4
|
||||
#define MB_FC_WRITE_COIL 5
|
||||
#define MB_FC_WRITE_REGISTER 6
|
||||
#define MB_FC_WRITE_HOLDING_REGISTER 6
|
||||
#define MB_FC_WRITE_MULTIPLE_COILS 15
|
||||
#define MB_FC_WRITE_MULTIPLE_REGISTERS 16
|
||||
#define MB_FC_ERROR 255
|
||||
|
@ -65,6 +65,7 @@ using namespace std;
|
|||
/// \param mb_error
|
||||
int modbus_error(unsigned char *buffer, int mb_error)
|
||||
{
|
||||
spdlog::warn("Error encountered in modbus slave {}", mb_error);
|
||||
buffer[4] = 0;
|
||||
buffer[5] = 3;
|
||||
buffer[7] = buffer[7] | 0x80; //set the highest bit
|
||||
|
@ -242,7 +243,7 @@ int write_holding_register(unsigned char* buffer, int buffer_size, IndexedStrate
|
|||
{
|
||||
int16_t start = mb_to_word(buffer[8], buffer[9]);
|
||||
|
||||
modbus_errno err = strategy->WriteHoldingRegisters(start, 1, buffer + 9);
|
||||
modbus_errno err = strategy->WriteHoldingRegisters(start, 1, buffer + 10);
|
||||
if (err)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_ADDRESS);
|
||||
|
@ -266,7 +267,8 @@ int write_multiple_coils(unsigned char* buffer, int buffer_size, IndexedStrategy
|
|||
}
|
||||
|
||||
// Check that we have enough bytes
|
||||
if (buffer_size < (byte_data_length + 13) || buffer[12] != byte_data_length) {
|
||||
if (buffer_size < (byte_data_length + 13) || buffer[12] != byte_data_length)
|
||||
{
|
||||
return modbus_error(buffer, ERR_ILLEGAL_DATA_VALUE);
|
||||
}
|
||||
|
||||
|
@ -325,6 +327,8 @@ int16_t modbus_process_message(unsigned char *buffer, int16_t buffer_size, void*
|
|||
return modbus_error(buffer, ERR_ILLEGAL_FUNCTION);
|
||||
}
|
||||
|
||||
spdlog::trace("Modbus slave message function {} received", buffer[7]);
|
||||
|
||||
switch (buffer[7])
|
||||
{
|
||||
case MB_FC_READ_COILS:
|
||||
|
@ -337,7 +341,7 @@ int16_t modbus_process_message(unsigned char *buffer, int16_t buffer_size, void*
|
|||
return read_input_registers(buffer, buffer_size, strategy);
|
||||
case MB_FC_WRITE_COIL:
|
||||
return write_coil(buffer, buffer_size, strategy);
|
||||
case MB_FC_WRITE_REGISTER:
|
||||
case MB_FC_WRITE_HOLDING_REGISTER:
|
||||
return write_holding_register(buffer, buffer_size, strategy);
|
||||
case MB_FC_WRITE_MULTIPLE_COILS:
|
||||
return write_multiple_coils(buffer, buffer_size, strategy);
|
||||
|
@ -365,7 +369,6 @@ void* modbus_exchange_data(void* args)
|
|||
|
||||
while (*exchange_args->run)
|
||||
{
|
||||
spdlog::trace("Exchanging modbus master data");
|
||||
exchange_args->strategy->Exchange();
|
||||
this_thread::sleep_for(exchange_args->interval);
|
||||
}
|
||||
|
@ -456,7 +459,6 @@ int8_t modbus_slave_run(oplc::config_stream& cfg_stream,
|
|||
|
||||
ini_parse_stream(oplc::istream_fgets, cfg_stream.get(),
|
||||
modbus_slave_cfg_handler, &config);
|
||||
|
||||
cfg_stream.reset(nullptr);
|
||||
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
@ -469,7 +471,7 @@ int8_t modbus_slave_run(oplc::config_stream& cfg_stream,
|
|||
.interval=std::chrono::milliseconds(100)
|
||||
};
|
||||
|
||||
int ret = pthread_create(&exchange_data_thread, NULL, modbus_exchange_data, args);
|
||||
int ret = pthread_create(&exchange_data_thread, nullptr, modbus_exchange_data, args);
|
||||
if (ret == 0)
|
||||
{
|
||||
pthread_detach(exchange_data_thread);
|
||||
|
|
|
@ -195,36 +195,45 @@ void *handleConnections(void *arguments)
|
|||
auto args = reinterpret_cast<ServerArgs*>(arguments);
|
||||
|
||||
unsigned char buffer[NET_BUFFER_SIZE];
|
||||
int messageSize;
|
||||
int message_size;
|
||||
int client_fd = args->client_fd;
|
||||
|
||||
spdlog::debug("Server: Thread created for client ID: {}", args->client_fd);
|
||||
spdlog::debug("Server: Thread created for client ID: {}", client_fd);
|
||||
|
||||
while(*args->run)
|
||||
{
|
||||
messageSize = listenToClient(args->client_fd, buffer);
|
||||
if (messageSize <= 0 || messageSize > NET_BUFFER_SIZE)
|
||||
message_size = listenToClient(client_fd, buffer);
|
||||
if (message_size <= 0 || message_size > NET_BUFFER_SIZE)
|
||||
{
|
||||
// something has gone wrong or the client has closed connection
|
||||
if (messageSize == 0)
|
||||
if (message_size == 0)
|
||||
{
|
||||
spdlog::debug("Server: client ID: {} has closed the connection", args->client_fd);
|
||||
spdlog::debug("Server: client ID: {} has closed the connection", client_fd);
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::error("Server: Something is wrong with the client ID: {} message Size : {}", args->client_fd, messageSize);
|
||||
spdlog::error("Server: Something is wrong with the client ID: {} message Size : {}", client_fd, message_size);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int messageSize = args->process_message(buffer, NET_BUFFER_SIZE, args->user_data);
|
||||
write(args->client_fd, buffer, messageSize);
|
||||
spdlog::trace("Message received for client {}", client_fd);
|
||||
int response_size = args->process_message(buffer, NET_BUFFER_SIZE, args->user_data);
|
||||
spdlog::trace("Message processing completed for client {}", client_fd);
|
||||
auto result = write(client_fd, buffer, response_size);
|
||||
|
||||
if (!result) {
|
||||
spdlog::warn("Unable to write to client {}", client_fd);
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::debug("Closing client socket and calling pthread_exit");
|
||||
spdlog::trace("Closing client socket and calling pthread_exit");
|
||||
close(args->client_fd);
|
||||
spdlog::info("Terminating server connections thread");
|
||||
spdlog::trace("Terminating server connections thread");
|
||||
pthread_exit(NULL);
|
||||
delete args;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// @brief Function to start a socket server.
|
||||
|
@ -258,7 +267,7 @@ void startServer(uint16_t port, volatile bool& run_server, process_message_fn pr
|
|||
.process_message=process_message,
|
||||
.user_data=user_data
|
||||
};
|
||||
spdlog::trace("Server: Client accepted! Creating thread for the new client ID: {}...", client_fd);
|
||||
spdlog::trace("Server: Client accepted on {}! Creating thread for the new client ID: {}...", port, client_fd);
|
||||
int success = pthread_create(&thread, NULL, handleConnections, args);
|
||||
if (success == 0)
|
||||
{
|
||||
|
|
|
@ -160,6 +160,7 @@ void ServiceDefinition::after_cycle()
|
|||
void* ServiceDefinition::run_service(void* user_data)
|
||||
{
|
||||
auto service = reinterpret_cast<ServiceDefinition*>(user_data);
|
||||
spdlog::debug("Service {} thread started", service->name);
|
||||
|
||||
GlueVariablesBinding bindings(&bufferLock, OPLCGLUE_GLUE_SIZE,
|
||||
oplc_glue_vars, OPLCGLUE_MD5_DIGEST);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
[logging]
|
||||
level = info
|
||||
[modbusslave]
|
||||
enabled = true
|
||||
port = 2502
|
||||
address = 127.0.0.1
|
||||
binding = sized
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
DiscreteInput AT %IX0.0 : BOOL;
|
||||
END_VAR
|
||||
|
||||
DiscreteInput := FALSE;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
DiscreteInput AT %IX0.0 : BOOL;
|
||||
END_VAR
|
||||
|
||||
DiscreteInput := TRUE;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
HoldingRegisterInt AT %MD0 : DINT;
|
||||
END_VAR
|
||||
|
||||
HoldingRegisterInt := HoldingRegisterInt;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
HoldingRegisterInt AT %ML0 : LINT;
|
||||
END_VAR
|
||||
|
||||
HoldingRegisterInt := HoldingRegisterInt;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
HoldingRegisterInt AT %MW0 : INT;
|
||||
END_VAR
|
||||
|
||||
HoldingRegisterInt := HoldingRegisterInt;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
HoldingRegisterInt AT %QW0 : INT;
|
||||
END_VAR
|
||||
|
||||
HoldingRegisterInt := HoldingRegisterInt;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,19 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
Coil0 AT %QX0.0 : BOOL;
|
||||
Coil1 AT %QX0.1 : BOOL;
|
||||
Coil2 AT %QX0.2 : BOOL;
|
||||
END_VAR
|
||||
|
||||
Coil0 := Coil0;
|
||||
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,16 @@
|
|||
PROGRAM program0
|
||||
VAR
|
||||
Coil AT %QX0.0 : BOOL;
|
||||
END_VAR
|
||||
|
||||
Coil := Coil;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
|
||||
PROGRAM instance0 WITH task0 : program0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2019 Garret Fick
|
||||
|
||||
# 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 permissionsand
|
||||
# limitations under the License.
|
||||
|
||||
pymodbus
|
|
@ -0,0 +1,2 @@
|
|||
[interactive]
|
||||
enabled = true
|
|
@ -0,0 +1,17 @@
|
|||
PROGRAM prog0
|
||||
VAR
|
||||
var_in : BOOL;
|
||||
var_out : BOOL;
|
||||
END_VAR
|
||||
|
||||
var_out := var_in;
|
||||
END_PROGRAM
|
||||
|
||||
|
||||
CONFIGURATION Config0
|
||||
|
||||
RESOURCE Res0 ON PLC
|
||||
TASK Main(INTERVAL := T#50ms,PRIORITY := 0);
|
||||
PROGRAM Inst0 WITH Main : prog0;
|
||||
END_RESOURCE
|
||||
END_CONFIGURATION
|
|
@ -1,54 +0,0 @@
|
|||
# Copyright 2019 Smarter Grid Solutions
|
||||
|
||||
# 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 permissionsand
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
|
||||
|
||||
class IntegrationTest(unittest.TestCase):
|
||||
|
||||
def test_connect_and_get_logs(self):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(('127.0.0.1', 43628))
|
||||
s.sendall('runtime_logs()\n')
|
||||
data = s.recv(1)
|
||||
print(data)
|
||||
s.close()
|
||||
|
||||
def test_connect_with_modbus(self):
|
||||
client = ModbusClient('localhost', port=502)
|
||||
unit=0x01
|
||||
|
||||
# Write the value true to the coil
|
||||
client.write_coil(1, True, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
|
||||
self.assertTrue(bit_value)
|
||||
|
||||
# Write the value false to the coil
|
||||
client.write_coil(1, False, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
|
||||
self.assertFalse(bit_value)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,288 @@
|
|||
# Copyright 2019 Garret Fick
|
||||
|
||||
# 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 permissionsand
|
||||
# limitations under the License.
|
||||
"""
|
||||
This test module defines a series of integration tests.
|
||||
These tests compile the runtime with a specified ST input
|
||||
and then validate some aspect of the runtime. In generate, these
|
||||
are focused on the protocol implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
build_dir = os.path.abspath(os.path.join("..", "..", "build"))
|
||||
bin_dir = os.path.abspath(os.path.join("..", "..", "bin"))
|
||||
|
||||
class TestProtocols(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.process = None
|
||||
self.socket = None
|
||||
self.client = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.process is not None:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
if self.socket is not None:
|
||||
self.socket.close()
|
||||
if self.client is not None:
|
||||
self.client.close()
|
||||
|
||||
def compile_and_run(self, st_filename, config_filename):
|
||||
"""
|
||||
Compile the runtime with the specified ST input, then start.
|
||||
|
||||
This gives a simple way to start the runtime for testing. We expect
|
||||
that the files are in the same directory as this test.
|
||||
"""
|
||||
|
||||
st_filepath = os.path.join(here, st_filename)
|
||||
config_filepath = os.path.join(here, config_filename)
|
||||
|
||||
if not os.path.exists(build_dir):
|
||||
os.mkdir(build_dir)
|
||||
|
||||
# Generate the environment
|
||||
cmake_cmd = [
|
||||
"cmake",
|
||||
"..",
|
||||
"-DUSER_STSOURCE_DIR=" + os.path.dirname(st_filepath),
|
||||
"-Dprogram_name=" + os.path.basename(st_filepath)
|
||||
]
|
||||
subprocess.run(cmake_cmd, cwd=build_dir, check=True)
|
||||
|
||||
# Build the files
|
||||
subprocess.run(["make"], cwd=build_dir)
|
||||
|
||||
# Start the runtime
|
||||
self.process = subprocess.Popen(
|
||||
[os.path.join(bin_dir, "openplc"), "--config", config_filepath],
|
||||
# We run with this as our current working directory because
|
||||
# We expect that the runtime should work correctly irrespective
|
||||
# of the current directory we had at start
|
||||
cwd=here
|
||||
)
|
||||
|
||||
return self.process
|
||||
|
||||
def compile_and_run_modbus(self, st_filename):
|
||||
self.compile_and_run(st_filename, "modbusslave.ini")
|
||||
time.sleep(2)
|
||||
|
||||
# Create a master that will connect to the modbus slave. We use
|
||||
# port 2502 so that we don't need to run as root
|
||||
self.client = ModbusClient('localhost', port=2502)
|
||||
return self.client
|
||||
|
||||
def test_modbus_slave_singlecoil(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_singlecoil.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the coil to true
|
||||
resp = client.write_coil(0, True, unit=unit)
|
||||
self.assertEqual(1, resp.value)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
self.assertTrue(bit_value)
|
||||
|
||||
# Write the value false to the coil
|
||||
client.write_coil(0, False, unit=unit)
|
||||
#self.assertEqual(0, resp.value)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports false
|
||||
rr = client.read_coils(0, 1, unit=unit)
|
||||
bit_value = rr.getBit(0)
|
||||
self.assertFalse(bit_value)
|
||||
|
||||
def disabled_test_modbus_slave_multiplecoils(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_multiplecoils.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the coil to true
|
||||
rq = client.write_coils(0, [False]*8, unit=unit)
|
||||
self.assertTrue(rq.function_code < 0x80)
|
||||
time.sleep(2)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_coils(0, 8, unit=unit)
|
||||
print(rr)
|
||||
self.assertEqual(rr.bits, [False]*8)
|
||||
|
||||
def test_modbus_slave_single_discrete_input_true(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_discreteinput_true.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the coil to true
|
||||
resp = client.read_discrete_inputs(0, 1, unit=unit)
|
||||
bit_value = resp.getBit(0)
|
||||
self.assertTrue(bit_value)
|
||||
|
||||
def test_modbus_slave_single_discrete_input_false(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_discreteinput_false.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the coil to true
|
||||
resp = client.read_discrete_inputs(0, 1, unit=unit)
|
||||
bit_value = resp.getBit(0)
|
||||
self.assertFalse(bit_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_qw(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_qw.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(0, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(0, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_mw(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_mw.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(1024, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(1024, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_md_highbytes(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_md.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(2048, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(2048, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_md_lowbytes(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_md.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(2049, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(2049, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_ml_byte1(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_ml.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(4096, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(4096, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_ml_byte2(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_ml.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(4097, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(4097, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_ml_byte3(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_ml.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(4098, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(4098, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def test_modbus_slave_holding_registers_ml_byte4(self):
|
||||
client = self.compile_and_run_modbus("modbusslave_holdingregister_ml.st")
|
||||
unit=0x01
|
||||
|
||||
# Set the holding register value - this is function code 6
|
||||
resp = client.write_register(4099, 10, unit=unit)
|
||||
time.sleep(1)
|
||||
|
||||
# Validate that the coil now reports true
|
||||
rr = client.read_holding_registers(4099, 1, unit=unit)
|
||||
reg_value = rr.registers[0]
|
||||
self.assertEqual(10, reg_value)
|
||||
|
||||
def socket_connect_and_run(self, command):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect(("127.0.0.1", 43628))
|
||||
self.socket.sendall(str.encode(command))
|
||||
data = self.socket.recv(1000)
|
||||
return data.decode()
|
||||
|
||||
def atest_interactiveserver_runtimelogs(self):
|
||||
proc = self.compile_and_run("socketserver.st", "socketserver.ini")
|
||||
|
||||
data = self.socket_connect_and_run("runtime_logs()\n")
|
||||
# We should be able to get the logs and the logs should
|
||||
# have this message in it
|
||||
self.assertTrue("OpenPLC Runtime starting..." in data)
|
||||
|
||||
def atest_interactiveserver_exectime(self):
|
||||
proc = self.compile_and_run("socketserver.st", "socketserver.ini")
|
||||
|
||||
data = self.socket_connect_and_run("exec_time()\n")
|
||||
|
||||
# The execution time should be greater than 0, but by how much
|
||||
# we cannot tell
|
||||
val = int(data)
|
||||
self.assertTrue(val > 0)
|
||||
|
||||
def atest_interactiveserver_quit(self):
|
||||
proc = self.compile_and_run("socketserver.st", "socketserver.ini")
|
||||
|
||||
data = self.socket_connect_and_run("quit()\n")
|
||||
|
||||
self.assertTrue("OK" in data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -29,7 +29,7 @@ endif()
|
|||
|
||||
# This is all of our test files
|
||||
file(GLOB oplctest_SRC *.cpp **/*.cpp)
|
||||
file(GLOB oplc_core_SRC ../core/pstorage.cpp ../core/server.cpp ../core/dnp3s/*.cpp ../core/modbusslave/*.cpp)
|
||||
file(GLOB oplc_core_SRC ../core/ini_util.cpp ../core/pstorage.cpp ../core/server.cpp ../core/dnp3s/*.cpp ../core/modbusslave/*.cpp)
|
||||
|
||||
add_executable(oplc_unit_test ${oplctest_SRC} ${oplc_core_SRC} ../core/glue.cpp ../vendor/inih-r46/ini.c)
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ SCENARIO("indexed_strategy", "")
|
|||
{
|
||||
IEC_INT int_val(0);
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_WORD, 1024, 0, IECVT_INT, &int_val },
|
||||
{ IECLDT_MEM, IECLST_WORD, 0, 0, IECVT_INT, &int_val },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, nullptr);
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
@ -153,7 +153,7 @@ SCENARIO("indexed_strategy", "")
|
|||
{
|
||||
IEC_DINT dint_val(0);
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 2048, 0, IECVT_DINT, &dint_val },
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_DINT, &dint_val },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, nullptr);
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
@ -189,7 +189,7 @@ SCENARIO("indexed_strategy", "")
|
|||
{
|
||||
IEC_LINT lint_val(0);
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 4096, 0, IECVT_LINT, &lint_val },
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 0, 0, IECVT_LINT, &lint_val },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 1, glue_vars, nullptr);
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
|
|
@ -163,9 +163,9 @@ SCENARIO("modbusslave", "")
|
|||
IEC_LINT lint_val(4);
|
||||
const GlueVariable glue_vars[] = {
|
||||
{ IECLDT_OUT, IECLST_WORD, 0, 0, IECVT_INT, &int_val1 },
|
||||
{ IECLDT_MEM, IECLST_WORD, 1024, 0, IECVT_INT, &int_val2 },
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 2048, 0, IECVT_DINT, &dint_val },
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 4096, 0, IECVT_LINT, &lint_val },
|
||||
{ IECLDT_MEM, IECLST_WORD, 0, 0, IECVT_INT, &int_val2 },
|
||||
{ IECLDT_MEM, IECLST_DOUBLEWORD, 0, 0, IECVT_DINT, &dint_val },
|
||||
{ IECLDT_MEM, IECLST_LONGWORD, 0, 0, IECVT_LINT, &lint_val },
|
||||
};
|
||||
GlueVariablesBinding bindings(&glue_mutex, 4, glue_vars, nullptr);
|
||||
IndexedStrategy strategy(bindings);
|
||||
|
|
Loading…
Reference in New Issue