Skip to content

Commit

Permalink
Added some test infra and github ci
Browse files Browse the repository at this point in the history
  • Loading branch information
Ómar Högni Guðmarsson committed Aug 10, 2023
1 parent 5180f4a commit bddc954
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 85 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: build
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
name: 'build'
runs-on: ${{matrix.os}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-11.0, windows-latest]
build_type: [debug, release]
steps:
- uses: actions/checkout@v3
- name: Restore artifacts, or setup vcpkg
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: 'fbc868ee5e12e3e81f464104be246ec06553c274'
vcpkgJsonGlob: 'vcpkg.json'
- name: build with cmake
uses: lukka/run-cmake@v10
with:
buildPreset: 'build-ci'
buildPresetAdditionalArgs: "['--config ${{matrix.build_type}}']"
testPreset: 'test-ci'
testPresetAdditionalArgs: "['--config ${{matrix.build_type}}']"
configurePreset: 'conf-ci'
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
vcpkg-sys*
build
build*/
cmake-build*
.idea
vcpkg/
.vscode
Expand Down
15 changes: 12 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@ set(CMAKE_CXX_STANDARD 23)

find_package(Boost REQUIRED)

include(FeatureSummary)

option(BUILD_TESTS "Indicates whether tests should be built." ON)
add_feature_info("BUILD_TESTS" BUILD_TESTS "Indicates whether tests should be built.")

add_library(modbus
STATIC
src/client.cpp
src/error.cpp
src/server.cpp)

target_link_libraries(modbus PRIVATE Boost::boost)
target_include_directories(modbus PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/)
target_include_directories(modbus PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)

add_executable(modbus_test src/test.cpp)
target_link_libraries(modbus_test PRIVATE modbus pthread Boost::boost)
target_link_libraries(modbus_test PRIVATE modbus Boost::boost)

add_executable(modbus_server_test src/server_test.cpp)
target_link_libraries(modbus_server_test PRIVATE modbus pthread Boost::boost)
target_link_libraries(modbus_server_test PRIVATE modbus Boost::boost)

if(BUILD_TESTS)
add_subdirectory(tests)
endif()
26 changes: 26 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": 6,
"configurePresets": [
{
"name": "conf-ci",
"generator": "Ninja",
"cacheVariables": {
"BUILD_SHARED_LIBS": "ON",
"CMAKE_TOOLCHAIN_FILE": "${GITHUB_WORKSPACE}/vcpkg/scripts/buildsystems/vcpkg.cmake"
}
}
],
"buildPresets": [
{
"name": "build-ci",
"configurePreset": "conf-ci"
}
],
"testPresets": [
{
"name": "test-ci",
"hidden": true,
"output": {"outputOnFailure": true},
"execution": {"noTestsAction": "error", "stopOnFailure": true}
}
}
69 changes: 30 additions & 39 deletions include/modbus/server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,6 @@ namespace modbus {
return error_buffer;
}

std::array<asio::const_buffer, 2> encode_resp(std::vector<uint8_t> resp, tcp_mbap *header) {
header->length = resp.size() + 1;
header->length = htons(header->length);
header->protocol = htons(header->protocol);
header->transaction = htons(header->transaction);
std::array<asio::const_buffer, 2> buffs{
asio::buffer(std::launder(reinterpret_cast<uint8_t *>(header)), tcp_mbap::size), asio::buffer(resp)};
return buffs;
}

awaitable<void> handle_connection(tcp::socket client, auto &&handler) {
auto state = std::make_shared<connection_state>(std::move(client));
Expand All @@ -104,7 +95,7 @@ namespace modbus {
auto result = co_await (
state->client_.async_read_some(asio::buffer(header_buffer, tcp_mbap::size),
asio::as_tuple(asio::use_awaitable)) ||
timeout(60s));
timeout(5s));
if (result.index() == 1) {
// Timeout
std::cerr << "timeout client: " << state->client_.remote_endpoint() << " Disconnecting!" << std::endl;
Expand All @@ -121,75 +112,75 @@ namespace modbus {
break;
}
// Deserialize the request
auto* header = tcp_mbap::from_bytes(header_buffer); //std::launder(reinterpret_cast<tcp_mbap *>(header_buffer.data()));
//header->transaction = ntohs(header->transaction);
//header->protocol = ntohs(header->protocol);
//header->length = ntohs(header->length);
std::cout << *header << std::endl;
auto header = tcp_mbap::from_bytes(header_buffer);
std::cout << header << std::endl;

if (header->length < 2) {
if (header.length < 2) {
co_await async_write(state->client_,
asio::buffer(build_error_buffer(*header, 0, errc::illegal_function), count),
asio::buffer(build_error_buffer(header, 0, errc::illegal_function), count),
use_awaitable);
continue;
}

// Read the request body
auto [request_ec, request_count] = co_await state->client_.async_read_some(
asio::buffer(request_buffer, header->length - 1), asio::as_tuple(asio::use_awaitable));
asio::buffer(request_buffer, header.length - 1), asio::as_tuple(asio::use_awaitable));
if (request_ec) {
std::cerr << "error client: " << state->client_.remote_endpoint() << " Disconnecting!" << std::endl;
break;
}

if (request_count < header->length - 1) {
if (request_count < header.length - 1) {
std::cerr << "packet size to small for body " << request_count << " "
<< state->client_.remote_endpoint() << " Disconnecting!" << std::endl;
co_await async_write(state->client_,
asio::buffer(build_error_buffer(*header, 0, errc::illegal_data_value), count),
asio::buffer(build_error_buffer(header, 0, errc::illegal_data_value), count),
use_awaitable);
continue;
}

uint8_t function_code = request_buffer[0];
//co_await async_write(state->client_, encode_resp(resp, header), use_awaitable);
// Handle the request
auto resp = [&]() -> std::expected<std::vector<uint8_t>, modbus::errc_t> {
switch (function_code) {
case functions::read_coils:
return handle_request<request::read_coils>(*header, request_buffer.data(), request_count,
return handle_request<request::read_coils>(header, request_buffer.data(), request_count,
handler);
case functions::read_discrete_inputs:;
return handle_request<request::read_discrete_inputs>(*header, request_buffer.data(),
case functions::read_discrete_inputs:
return handle_request<request::read_discrete_inputs>(header, request_buffer.data(),
request_count, handler);
case functions::read_holding_registers:;
return handle_request<request::read_holding_registers>(*header, request_buffer.data(),
case functions::read_holding_registers:
return handle_request<request::read_holding_registers>(header, request_buffer.data(),
request_count, handler);
case functions::read_input_registers:;
return handle_request<request::read_input_registers>(*header, request_buffer.data(),
case functions::read_input_registers:
return handle_request<request::read_input_registers>(header, request_buffer.data(),
request_count, handler);
case functions::write_single_coil:;
return handle_request<request::write_single_coil>(*header, request_buffer.data(), request_count,
case functions::write_single_coil:
return handle_request<request::write_single_coil>(header, request_buffer.data(), request_count,
handler);
case functions::write_single_register:;
return handle_request<request::write_single_register>(*header, request_buffer.data(),
case functions::write_single_register:
return handle_request<request::write_single_register>(header, request_buffer.data(),
request_count, handler);
case functions::write_multiple_coils:;
return handle_request<request::write_multiple_coils>(*header, request_buffer.data(),
case functions::write_multiple_coils:
return handle_request<request::write_multiple_coils>(header, request_buffer.data(),
request_count, handler);
case functions::write_multiple_registers:;
return handle_request<request::write_multiple_registers>(*header, request_buffer.data(),
case functions::write_multiple_registers:
return handle_request<request::write_multiple_registers>(header, request_buffer.data(),
request_count, handler);
default:
return std::unexpected(errc::illegal_function);
}
}();
if (resp) {
size_t n = co_await async_write(state->client_, encode_resp(resp.value(), header), use_awaitable);
header.length = resp.value().size() + 1;
auto header_bytes = header.to_bytes();
std::array<asio::const_buffer, 2> buffs{
asio::buffer(header_bytes), asio::buffer(resp.value())};
size_t n = co_await async_write(state->client_, buffs, use_awaitable);
std::cout << "Wrote " << n << "\n";
} else {
co_await async_write(state->client_,
asio::buffer(build_error_buffer(*header, 0, resp.error()), count),
asio::buffer(build_error_buffer(header, 0, resp.error()), count),
use_awaitable);
}
}
Expand Down Expand Up @@ -219,7 +210,7 @@ namespace modbus {
client.set_option(asio::ip::tcp::no_delay(true));
client.set_option(asio::socket_base::keep_alive(true));

std::cout << "Connection opened from " << client.remote_endpoint() << "\n";
std::cout << "Connection opened from " << client.remote_endpoint() << std::endl;

co_spawn(acceptor_.get_executor(), handle_connection(std::move(client), handler_), detached);

Expand Down
61 changes: 19 additions & 42 deletions include/modbus/tcp.hpp
Original file line number Diff line number Diff line change
@@ -1,62 +1,39 @@
// Copyright (c) 2017, Fizyr (https://fizyr.com)
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the copyright holder(s) nor the
// names of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#pragma once
#include <cstdint>
#include <utility>
#include <boost/asio.hpp>

namespace modbus {

/// Modbus/TCP application protocol (MBAP) header.
/**
* The TCP MBAP contains fields specific to Modbus/TCP.
* Together with a regular Modbus application data unit (ADU) it forms
* a Modbus/TCP protocol data unit (PDU).
*/
#pragma pack(push, 1)
struct tcp_mbap {
/// Transaction identifier.
std::uint16_t transaction;

/// Protocol identifier. Set to 0 for modbus.
std::uint16_t protocol = 0;

/// Length of payload + unit identifier.
std::uint16_t protocol = 0; // 0 for modbus.
std::uint16_t length;

/// Unit identifier.
std::uint8_t unit;

/// Header size
static constexpr size_t size = 7;

static tcp_mbap* from_bytes(std::span<uint8_t> raw_bytes) {
static tcp_mbap from_bytes(const std::span<uint8_t> raw_bytes) {
auto header = std::launder(reinterpret_cast<tcp_mbap*>(raw_bytes.data()));
header->transaction = ntohs(header->transaction);
tcp_mbap ret_header;
ret_header.transaction = ntohs(header->transaction);
// header->protocol This is always zero
ret_header.length = ntohs(header->length);
ret_header.unit = header->unit;
return ret_header;
}

[[nodiscard]] std::array<uint8_t, size> to_bytes() const {
auto bytes = std::array<uint8_t, size>{};
auto* header = std::launder(reinterpret_cast<tcp_mbap*>(bytes.data()));
header->transaction = htons(transaction);
// header->protocol This is always zero
header->length = ntohs(header->length);
return header;
header->length = htons(length);
header->unit = unit;
return bytes;
}
};
#pragma pack(pop)
Expand All @@ -70,7 +47,7 @@ std::ostream& operator<<(std::ostream& out, const tcp_mbap& inst){
return out;
}

static_assert(sizeof(tcp_mbap) == 7, "tcp_mbap has incorrect size");
static_assert(sizeof(tcp_mbap) == tcp_mbap::size, "tcp_mbap has incorrect size");

/// Modbus/TCP protocol data unit (PDU).
/**
Expand Down
8 changes: 8 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enable_testing()

find_package(ut CONFIG REQUIRED)
add_executable(serialization serialization.cpp)

target_link_libraries(serialization PRIVATE Boost::ut modbus)

add_test(NAME serialization COMMAND serialization)
35 changes: 35 additions & 0 deletions tests/serialization.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <array>

#include <boost/ut.hpp>
#include <modbus/tcp.hpp>

void print_bytes(std::span<uint8_t> data){
for (auto& k : data) {
std::cout << static_cast<int>(k) << " ";
}
std::cout << std::endl;
}

int main(){
using boost::ut::operator""_test;
using boost::ut::expect;

"Modbus tcp header serialize/deserialize"_test = []() {
using modbus::tcp_mbap;
tcp_mbap header;
auto bytes = std::array<uint8_t, tcp_mbap::size>{0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01};
auto hd = tcp_mbap::from_bytes(bytes);

expect(hd.transaction == 1);
expect(hd.protocol == 0);
expect(hd.length == 6);
expect(hd.unit == 1);

auto buffer = hd.to_bytes();
print_bytes(buffer);
print_bytes(bytes);
for (size_t i = 0; i < buffer.size(); i++)
expect(buffer[i] == bytes[i]);
};
return 0;
}
9 changes: 9 additions & 0 deletions vcpkg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "modbus",
"version": "0.0.1",
"description": "Modbus protocol implementation, based on boost asio",
"dependencies": [
"boost-asio",
"bext-ut"
]
}

0 comments on commit bddc954

Please sign in to comment.