Add integration tests for interactive server and modbus. Fix discovered issues

This commit is contained in:
Garret Fick 2019-12-08 17:24:53 -05:00
parent 3fcc69f1cd
commit 2bf679df8a
No known key found for this signature in database
GPG Key ID: 0F2FA2774E86EEFF
27 changed files with 614 additions and 126 deletions

View File

@ -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

View File

@ -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.

View File

@ -102,7 +102,7 @@ void bootstrap()
// We just assume that the file we are reading with the
// configuration information in in the etc subfolder and use
// a relative path to find it.
const char* config_path = "../etc/config.ini";
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", config_path);

51
runtime/core/ini_util.cpp Normal file
View File

@ -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

View File

@ -28,7 +28,22 @@
namespace oplc
{
/// Convert a boolean value in the INI file to a boolean.
/// @brief Gets the path to the INI file.
const char* get_config_path();
/// @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.
@ -37,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.
@ -119,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("../etc/config.ini"),
[] (std::istream* s)
{
reinterpret_cast<std::ifstream*>(s)->close();
delete s;
}
);
}
config_stream open_config();
} // namespace oplc

View File

@ -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();

View File

@ -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

View File

@ -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);

View File

@ -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)
{

View File

@ -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);

View File

@ -0,0 +1,7 @@
[logging]
level = info
[modbusslave]
enabled = true
port = 2502
address = 127.0.0.1
binding = sized

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
[interactive]
enabled = true

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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);

View File

@ -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);