Support IPMI Serial Transport

Support IPMI Serial interface (basic mode) based on Sec 14,
IPMI 2.0 spec. Serial transport tested with meta-evb-arm
platform with socat and ipmitool.

HW transport can be selected by meson configure flasg.

Tested:

1. socat -x pty,raw,echo=0,link=/tmp/ttyHOST2BMC tcp:localhost:5066
2. ipmitool -I serial-basic -D /tmp/ttyHOST2BMC:115200 mc info

    Device ID                 : 0
    Device Revision           : 0
    Firmware Revision         : 2.17
    IPMI Version              : 2.0
    Manufacturer ID           : 0
    Manufacturer Name         : Unknown
    Product ID                : 0 (0x0000)
    Product Name              : Unknown (0x0)
    Device Available          : no
    Provides Device SDRs      : no
    Additional Device Support :
    Aux Firmware Rev Info     :
        0x00
        0x00
        0x00
        0x00

Change-Id: I2a56390022fce7a974ed75ed6a72ad3fffed9bb6
Signed-off-by: John Chung <john.chung@arm.com>
diff --git a/meson.build b/meson.build
index a427bdb..aa6da2a 100644
--- a/meson.build
+++ b/meson.build
@@ -347,6 +347,7 @@
 
 if get_option('tests').allowed()
     subdir('test')
+    subdir('transport/serialbridge')
 endif
 
 install_subdir(
@@ -355,3 +356,6 @@
     strip_directory: false,
     exclude_files: '*.cpp',
 )
+
+# HW Transport
+subdir('transport')
diff --git a/meson.options b/meson.options
index 6d75e73..5a21171 100644
--- a/meson.options
+++ b/meson.options
@@ -243,3 +243,11 @@
     type: 'feature',
     description: 'Support open-power specific functions',
 )
+
+# HW transport
+option(
+    'transport-implementation',
+    type: 'combo',
+    choices: ['null', 'serial'],
+    description: 'transport'
+)
diff --git a/transport/meson.build b/transport/meson.build
new file mode 100644
index 0000000..4011220
--- /dev/null
+++ b/transport/meson.build
@@ -0,0 +1,3 @@
+if get_option('transport-implementation') == 'serial'
+  subdir('serialbridge')
+endif
diff --git a/transport/serialbridge/meson.build b/transport/serialbridge/meson.build
new file mode 100644
index 0000000..2964206
--- /dev/null
+++ b/transport/serialbridge/meson.build
@@ -0,0 +1,38 @@
+CLI11_dep = dependency('CLI11')
+
+deps = [
+    dependency('libsystemd'),
+    dependency('systemd'),
+    sdeventplus_dep,
+    stdplus_dep,
+    sdbusplus_dep,
+    phosphor_logging_dep,
+    CLI11_dep
+]
+
+serialbridged = executable(
+    'serialbridged',
+    'serialbridged.cpp',
+    'serialcmd.cpp',
+    dependencies: deps,
+    install: true,
+    install_dir: get_option('libexecdir')
+)
+
+# Configure and install systemd unit files
+systemd = dependency('systemd')
+if systemd.found()
+    conf_data = configuration_data()
+    conf_data.set('BIN', get_option('prefix') / get_option('libexecdir') / serialbridged.name())
+    configure_file(
+        input: 'serialbridge@.service.in',
+        output: 'serialbridge@.service',
+        configuration: conf_data,
+        install: true,
+        install_dir: systemd.get_variable(pkgconfig: 'systemdsystemunitdir')
+    )
+endif
+
+if not get_option('tests').disabled()
+    subdir('test')
+endif
diff --git a/transport/serialbridge/serialbridge@.service.in b/transport/serialbridge/serialbridge@.service.in
new file mode 100644
index 0000000..a7bfd45
--- /dev/null
+++ b/transport/serialbridge/serialbridge@.service.in
@@ -0,0 +1,18 @@
+[Unit]
+Description=Phosphor IPMI Serial DBus Bridge
+StartLimitBurst=3
+StartLimitIntervalSec=300
+After=phosphor-ipmi-host.service
+
+[Service]
+Restart=always
+RestartSec=10
+TimeoutStartSec=60
+TimeoutStopSec=60
+ExecStartPre=/bin/stty -F /dev/"%i" 115200 litout -crtscts -ixon -echo raw
+ExecStart=@BIN@ -d "%i"
+SyslogIdentifier=serialbridged-%i
+
+[Install]
+WantedBy=multi-user.target
+RequiredBy=
diff --git a/transport/serialbridge/serialbridged.cpp b/transport/serialbridge/serialbridged.cpp
new file mode 100644
index 0000000..b022dc6
--- /dev/null
+++ b/transport/serialbridge/serialbridged.cpp
@@ -0,0 +1,85 @@
+#include "serialcmd.hpp"
+
+#include <systemd/sd-daemon.h>
+
+#include <CLI/CLI.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/slot.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/source/io.hpp>
+#include <sdeventplus/source/signal.hpp>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/create.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/signal.hpp>
+
+namespace serialbridge
+{
+
+using sdeventplus::source::IO;
+using sdeventplus::source::Signal;
+using stdplus::fd::OpenAccess;
+using stdplus::fd::OpenFlag;
+using stdplus::fd::OpenFlags;
+
+int execute(const std::string& channel, const bool& verbose)
+{
+    // Set up DBus and event loop
+    auto event = sdeventplus::Event::get_default();
+    auto bus = sdbusplus::bus::new_default();
+    bus.attach_event(event.get(), SD_EVENT_PRIORITY_NORMAL);
+
+    // Configure basic signal handling
+    auto exit_handler = [&event](Signal&, const struct signalfd_siginfo*) {
+        lg2::error("Interrupted, Exiting\n");
+        event.exit(0);
+    };
+    stdplus::signal::block(SIGINT);
+    Signal sig_init(event, SIGINT, exit_handler);
+    stdplus::signal::block(SIGTERM);
+    Signal sig_term(event, SIGTERM, exit_handler);
+
+    // Open an FD for the UART channel
+    stdplus::ManagedFd uart = stdplus::fd::open(
+        std::format("/dev/{}", channel.c_str()),
+        OpenFlags(OpenAccess::ReadWrite).set(OpenFlag::NonBlock));
+    sdbusplus::slot_t slot(nullptr);
+
+    std::unique_ptr<SerialChannel> serialchannel =
+        std::make_unique<SerialChannel>(verbose);
+
+    // Add a reader to the bus for handling inbound IPMI
+    IO ioSource(event, uart.get(), EPOLLIN | EPOLLET,
+                stdplus::exception::ignore(
+                    [&serialchannel, &uart, &bus, &slot](IO&, int, uint32_t) {
+                        serialchannel->read(uart, bus, slot);
+                    }));
+
+    sd_notify(0, "READY=1");
+    return event.loop();
+}
+
+} // namespace serialbridge
+
+int main(int argc, char* argv[])
+{
+    std::string device;
+    bool verbose = 0;
+
+    // Parse input parameter
+    CLI::App app("Serial IPMI Bridge");
+    app.add_option("-d,--device", device, "select uart device");
+    app.add_option("-v,--verbose", verbose, "enable debug message");
+    CLI11_PARSE(app, argc, argv);
+
+    try
+    {
+        return serialbridge::execute(device, verbose);
+    }
+    catch (const std::exception& e)
+    {
+        lg2::error("FAILED: {MSG}\n", "MSG", e);
+        return 1;
+    }
+}
diff --git a/transport/serialbridge/serialcmd.cpp b/transport/serialbridge/serialcmd.cpp
new file mode 100644
index 0000000..ec84f5c
--- /dev/null
+++ b/transport/serialbridge/serialcmd.cpp
@@ -0,0 +1,340 @@
+#include "serialcmd.hpp"
+
+#include <fmt/format.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/slot.hpp>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/ops.hpp>
+
+#include <numeric>
+#include <ranges>
+#include <unordered_map>
+
+namespace serialbridge
+{
+
+/**
+ * @brief Table of special characters
+ */
+static const std::unordered_map<uint8_t, uint8_t> characters = {
+    {bmStart, 0xB0},     /* start */
+    {bmStop, 0xB5},      /* stop */
+    {bmHandshake, 0xB6}, /* packet handshake */
+    {bmEscape, 0xBA},    /* data escape */
+    {0x1B, 0x3B}         /* escape */
+};
+
+/**
+ * @brief Calculate IPMI checksum
+ */
+uint8_t SerialChannel::calculateChecksum(std::span<uint8_t> data)
+{
+    uint8_t checksum;
+
+    checksum = std::accumulate(data.begin(), data.end(), 0);
+    checksum = (~checksum) + 1;
+
+    // return checksum
+    return checksum;
+}
+
+/**
+ * @brief Return unescaped character for the given one
+ */
+uint8_t SerialChannel::getUnescapedCharacter(uint8_t c)
+{
+    auto search =
+        std::find_if(characters.begin(), characters.end(),
+                     [c](const auto& map_set) { return map_set.second == c; });
+
+    if (search == characters.end())
+    {
+        return c;
+    }
+
+    return search->first;
+}
+
+/**
+ * @brief Process IPMI Serial Request State Machine
+ */
+int SerialChannel::consumeIpmiSerialPacket(
+    std::span<uint8_t>& escapedDataBytes,
+    std::vector<uint8_t>& unescapedDataBytes)
+{
+    unescapedDataBytes.reserve(escapedDataBytes.size());
+
+    for (auto c : escapedDataBytes)
+    {
+        if (c == bmStart) // START
+        {
+            msgState = MsgState::msgInProgress;
+        }
+        else if (msgState == MsgState::msgIdle)
+        {
+            continue;
+        }
+        else if (msgState == MsgState::msgInEscape)
+        {
+            uint8_t unescapedCharacter;
+            unescapedCharacter = getUnescapedCharacter(c);
+
+            if (unescapedCharacter == c)
+            {
+                // error, then reset
+                msgState = MsgState::msgIdle;
+                unescapedDataBytes.clear();
+                continue;
+            }
+
+            unescapedDataBytes.push_back(unescapedCharacter);
+            msgState = MsgState::msgInProgress;
+        }
+        else if (c == bmEscape)
+        {
+            msgState = MsgState::msgInEscape;
+            continue;
+        }
+        else if (c == bmStop) // STOP
+        {
+            msgState = MsgState::msgIdle;
+            return true;
+        }
+        else if (c == bmHandshake) // Handshake
+        {
+            unescapedDataBytes.clear();
+            continue;
+        }
+        else if (msgState == MsgState::msgInProgress)
+        {
+            unescapedDataBytes.push_back(c);
+        }
+    }
+
+    return false;
+}
+
+/**
+ * @brief Encapsluate response to avoid escape character
+ */
+uint8_t SerialChannel::processEscapedCharacter(std::vector<uint8_t>& buffer,
+                                               const std::vector<uint8_t>& data)
+{
+    uint8_t checksum = 0;
+
+    std::ranges::for_each(data.begin(), data.end(),
+                          [&buffer, &checksum](const auto& c) {
+                              auto search = characters.find(c);
+                              if (search != characters.end())
+                              {
+                                  buffer.push_back(bmEscape);
+                                  buffer.push_back(search->second);
+                              }
+                              else
+                              {
+                                  buffer.push_back(c);
+                              }
+
+                              checksum += c;
+                          });
+
+    return checksum;
+}
+
+/**
+ * @brief Write function
+ */
+int SerialChannel::write(stdplus::Fd& uart, uint8_t rsAddr, uint8_t rqAddr,
+                         uint8_t seq, sdbusplus::message_t&& m)
+{
+    std::span<uint8_t> out;
+    uint8_t checksum;
+
+    try
+    {
+        if (m.is_method_error())
+        {
+            // Extra copy to workaround lack of `const sd_bus_error` constructor
+            auto error = *m.get_error();
+            throw sdbusplus::exception::SdBusError(&error, "ipmid response");
+        }
+
+        uint8_t netFn = 0xff;
+        uint8_t lun = 0xff;
+        uint8_t cmd = 0xff;
+        uint8_t cc = 0xff;
+        std::vector<uint8_t> data;
+
+        m.read(std::tie(netFn, lun, cmd, cc, data));
+
+        uint8_t netFnLun = (netFn << netFnShift) | (lun & lunMask);
+        uint8_t seqLun = (seq << netFnShift) | (lun & lunMask);
+
+        std::vector<uint8_t> connectionHeader = {rqAddr, netFnLun};
+        std::vector<uint8_t> messageHeader = {rsAddr, seqLun, cmd, cc};
+
+        // Reserve the buffer size to avoid relloc and copy
+        responseBuffer.clear();
+        responseBuffer.reserve(
+            sizeof(struct IpmiSerialHeader) + 2 * data.size() +
+            4); // 4 for bmStart & bmStop & 2 checksums
+
+        // bmStart
+        responseBuffer.push_back(bmStart);
+
+        // Assemble connection header and checksum
+        checksum = processEscapedCharacter(responseBuffer, connectionHeader);
+        responseBuffer.push_back(-checksum); // checksum1
+
+        // Assemble response message and checksum
+        checksum = processEscapedCharacter(responseBuffer, messageHeader);
+        checksum +=
+            processEscapedCharacter(responseBuffer, std::vector<uint8_t>(data));
+        responseBuffer.push_back(-checksum); // checksum2
+
+        // bmStop
+        responseBuffer.push_back(bmStop);
+
+        out = std::span<uint8_t>(responseBuffer.begin(), responseBuffer.end());
+
+        if (verbose)
+        {
+            lg2::info(
+                "Write serial request message with len={LEN}, netfn={NETFN}, "
+                "lun={LUN}, cmd={CMD}, seq={SEQ}",
+                "LEN", responseBuffer.size(), "NETFN", netFn, "LUN", lun, "CMD",
+                cmd, "SEQ", seq);
+
+            std::string msgData = "Tx: ";
+            for (auto c : responseBuffer)
+            {
+                msgData += std::format("{:#x} ", c);
+            }
+            lg2::info(msgData.c_str());
+        }
+
+        stdplus::fd::writeExact(uart, out);
+    }
+    catch (const std::exception& e)
+    {
+        lg2::error("IPMI Response failure: {MSG}", "MSG", e);
+
+        return -1;
+    }
+
+    return out.size();
+}
+
+/**
+ * @brief Read function
+ */
+void SerialChannel::read(stdplus::Fd& uart, sdbusplus::bus_t& bus,
+                         sdbusplus::slot_t& outstanding)
+{
+    std::array<uint8_t, ipmiSerialMaxBufferSize> buffer;
+    auto ipmiSerialPacket = stdplus::fd::read(uart, buffer);
+
+    if (ipmiSerialPacket.empty())
+    {
+        return;
+    }
+
+    if (outstanding)
+    {
+        lg2::error("Canceling outstanding request \n");
+        outstanding = sdbusplus::slot_t(nullptr);
+    }
+
+    // process ipmi serial packet
+    if (!consumeIpmiSerialPacket(ipmiSerialPacket, requestBuffer))
+    {
+        lg2::info("Wait for more data ... \n");
+        return;
+    }
+
+    // validate ipmi serial packet length
+    if (requestBuffer.size() <
+        (sizeof(struct IpmiSerialHeader) + ipmiSerialChecksumSize))
+    {
+        lg2::error("Invalid request length, ignoring \n");
+        requestBuffer.clear();
+        return;
+    }
+
+    // validate checksum1
+    if (calculateChecksum(std::span<uint8_t>(requestBuffer.begin(),
+                                             ipmiSerialConnectionHeaderLength)))
+    {
+        lg2::error("Invalid request checksum 1 \n");
+        requestBuffer.clear();
+        return;
+    }
+
+    // validate checksum2
+    if (calculateChecksum(std::span<uint8_t>(
+            &requestBuffer[ipmiSerialConnectionHeaderLength],
+            requestBuffer.size() - ipmiSerialConnectionHeaderLength)))
+    {
+        lg2::error("Invalid request checksum 2 \n");
+        requestBuffer.clear();
+        return;
+    }
+
+    auto m = bus.new_method_call("xyz.openbmc_project.Ipmi.Host",
+                                 "/xyz/openbmc_project/Ipmi",
+                                 "xyz.openbmc_project.Ipmi.Server", "execute");
+
+    std::map<std::string, std::variant<int>> options;
+    struct IpmiSerialHeader* header =
+        reinterpret_cast<struct IpmiSerialHeader*>(requestBuffer.data());
+
+    uint8_t rsAddr = header->rsAddr;
+    uint8_t netFn = header->rsNetFnLUN >> netFnShift;
+    uint8_t lun = header->rsNetFnLUN & lunMask;
+    uint8_t rqAddr = header->rqAddr;
+    uint8_t seq = header->rqSeqLUN >> netFnShift;
+    uint8_t cmd = header->cmd;
+
+    std::span reqSpan{requestBuffer.begin(),
+                      requestBuffer.end() -
+                          ipmiSerialChecksumSize}; // remove checksum 2
+    m.append(netFn, lun, cmd, reqSpan.subspan(sizeof(IpmiSerialHeader)),
+             options);
+
+    if (verbose)
+    {
+        lg2::info("Read serial request message with len={LEN}, netFn={NETFN}, "
+                  "lun={LUN}, cmd={CMD}, seq={SEQ}",
+                  "LEN", requestBuffer.size(), "NETFN", netFn, "LUN", lun,
+                  "CMD", cmd, "SEQ", seq);
+
+        std::string msgData = "Rx: ";
+        for (auto c : requestBuffer)
+        {
+            msgData += std::format("{:#x} ", c);
+        }
+        lg2::info(msgData.c_str());
+    }
+
+    outstanding = m.call_async(stdplus::exception::ignore(
+        [&outstanding, this, &uart, _rsAddr{rsAddr}, _rqAddr{rqAddr},
+         _seq{seq}](sdbusplus::message_t&& m) {
+            outstanding = sdbusplus::slot_t(nullptr);
+
+            if (write(uart, _rsAddr, _rqAddr, _seq, std::move(m)) < 0)
+            {
+                lg2::error(
+                    "Occur an error while attempting to send the response.");
+            }
+        }));
+
+    requestBuffer.clear();
+
+    return;
+}
+
+} // namespace serialbridge
diff --git a/transport/serialbridge/serialcmd.hpp b/transport/serialbridge/serialcmd.hpp
new file mode 100644
index 0000000..985921c
--- /dev/null
+++ b/transport/serialbridge/serialcmd.hpp
@@ -0,0 +1,64 @@
+#pragma once
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/slot.hpp>
+#include <stdplus/fd/intf.hpp>
+
+namespace serialbridge
+{
+
+static constexpr auto bmStart = 0xA0;
+static constexpr auto bmStop = 0xA5;
+static constexpr auto bmHandshake = 0xA6;
+static constexpr auto bmEscape = 0xAA;
+
+static constexpr auto ipmiSerialConnectionHeaderLength = 3;
+static constexpr auto ipmiSerialChecksumSize = 1;
+static constexpr auto ipmiSerialMaxBufferSize = 256;
+
+/**
+ * @brief IPMI Serial Message Structure
+ */
+struct IpmiSerialHeader
+{
+    uint8_t rsAddr;
+    uint8_t rsNetFnLUN;
+    uint8_t checksum1;
+    uint8_t rqAddr;
+    uint8_t rqSeqLUN;
+    uint8_t cmd;
+} __attribute__((packed));
+
+class SerialChannel
+{
+  public:
+    static constexpr uint8_t netFnShift = 2;
+    static constexpr uint8_t lunMask = (1 << netFnShift) - 1;
+
+    SerialChannel(bool debug) : verbose(debug), msgState(MsgState::msgIdle) {};
+
+    int write(stdplus::Fd& uart, uint8_t rsAddr, uint8_t rqAddr, uint8_t seq,
+              sdbusplus::message_t&& m);
+    void read(stdplus::Fd& serial, sdbusplus::bus_t& bus,
+              sdbusplus::slot_t& outstanding);
+    uint8_t calculateChecksum(std::span<uint8_t> data);
+    uint8_t getUnescapedCharacter(uint8_t c);
+    int consumeIpmiSerialPacket(std::span<uint8_t>& escapedDataBytes,
+                                std::vector<uint8_t>& unescapedDataBytes);
+    uint8_t processEscapedCharacter(std::vector<uint8_t>& buffer,
+                                    const std::vector<uint8_t>& data);
+
+  private:
+    bool verbose;
+    enum class MsgState
+    {
+        msgIdle = 0,
+        msgInProgress,
+        msgInEscape,
+    };
+    MsgState msgState;
+    std::vector<uint8_t> requestBuffer;
+    std::vector<uint8_t> responseBuffer;
+};
+
+} // namespace serialbridge
diff --git a/transport/serialbridge/test/meson.build b/transport/serialbridge/test/meson.build
new file mode 100644
index 0000000..d1ad4cf
--- /dev/null
+++ b/transport/serialbridge/test/meson.build
@@ -0,0 +1,42 @@
+gtest = dependency('gtest', main: true, disabler: true, required: false)
+gmock = dependency('gmock', disabler: true, required: false)
+if not gtest.found() or not gmock.found()
+    gtest_opts = import('cmake').subproject_options()
+    gtest_opts.add_cmake_defines({'CMAKE_CXX_FLAGS': '-Wno-pedantic'})
+    gtest_proj = import('cmake').subproject(
+        'googletest',
+        options: gtest_opts,
+        required: false
+    )
+    if gtest_proj.found()
+        gtest = declare_dependency(
+            dependencies: [
+                dependency('threads'),
+                gtest_proj.dependency('gtest'),
+                gtest_proj.dependency('gtest_main'),
+            ])
+        gmock = gtest_proj.dependency('gmock')
+    else
+        assert(not get_option('tests').enabled(), 'Googletest is required')
+    endif
+endif
+
+# Build/add serial_unittest to test suite
+test('transport_serial',
+    executable(
+        'transport_serial_unittest',
+        'serial_unittest.cpp',
+        '../serialcmd.cpp',
+        include_directories: root_inc,
+        build_by_default: false,
+        implicit_include_directories: false,
+        dependencies: [
+            sdbusplus_dep,
+            stdplus_dep,
+            phosphor_logging_dep,
+            sdeventplus_dep,
+            gtest,
+            gmock
+        ]
+    )
+)
diff --git a/transport/serialbridge/test/serial_unittest.cpp b/transport/serialbridge/test/serial_unittest.cpp
new file mode 100644
index 0000000..3fb0227
--- /dev/null
+++ b/transport/serialbridge/test/serial_unittest.cpp
@@ -0,0 +1,84 @@
+#include <transport/serialbridge/serialcmd.hpp>
+
+#include <gtest/gtest.h>
+
+namespace serialbridge
+{
+
+/**
+ * @brief Table of special characters
+ */
+std::unordered_map<uint8_t, uint8_t> testsets = {
+    {bmStart, 0xB0},     /* start */
+    {bmStop, 0xB5},      /* stop */
+    {bmHandshake, 0xB6}, /* packet handshake */
+    {bmEscape, 0xBA},    /* data escape */
+    {0x1B, 0x3B}         /* escape */
+};
+
+TEST(TestSpecialCharact, getUnescapedCharact)
+{
+    uint8_t c;
+    auto channel = std::make_shared<SerialChannel>(0);
+
+    for (const auto& set : testsets)
+    {
+        c = channel->getUnescapedCharacter(set.second);
+        ASSERT_EQ(c, set.first);
+    }
+}
+
+TEST(TestSpecialCharact, processEscapedCharacter)
+{
+    std::vector<uint8_t> buffer;
+    uint8_t unescaped = 0xd0;
+    auto channel = std::make_shared<SerialChannel>(0);
+
+    channel->processEscapedCharacter(buffer, std::vector<uint8_t>{bmStart});
+
+    ASSERT_EQ(buffer.at(0), bmEscape);
+    ASSERT_EQ(buffer.at(1), testsets.at(bmStart));
+
+    buffer.clear();
+    channel->processEscapedCharacter(buffer, std::vector<uint8_t>{unescaped});
+
+    ASSERT_EQ(buffer.at(0), unescaped);
+}
+
+TEST(TestChecksum, calculateChecksum)
+{
+    std::array<uint8_t, 5> dataBytes{0x01, 0x10, 0x60, 0xf0, 0x50};
+    auto channel = std::make_shared<SerialChannel>(0);
+
+    uint8_t checksum =
+        channel->calculateChecksum(std::span<uint8_t>(dataBytes));
+
+    checksum += (~checksum) + 1;
+    ASSERT_EQ(checksum, 0);
+}
+
+TEST(TestIpmiSerialPacket, consumeIpmiSerialPacket)
+{
+    std::vector<uint8_t> dataBytes{bmStart, 0x20, 0x18, 0xc8, 0x81,
+                                   0xc,     0x46, 0x01, 0x2c, bmStop};
+    std::vector<uint8_t> dataBytesSplit1{bmStart, 0x20, 0x18, 0xc8};
+    std::vector<uint8_t> dataBytesSplit2{0x81, 0xc, 0x46, 0x01, 0x2c, bmStop};
+    std::span<uint8_t> input(dataBytes);
+    std::span<uint8_t> input1(dataBytesSplit1);
+    std::span<uint8_t> input2(dataBytesSplit2);
+    std::vector<uint8_t> output;
+
+    auto channel = std::make_shared<SerialChannel>(0);
+
+    auto result = channel->consumeIpmiSerialPacket(input, output);
+
+    ASSERT_EQ(result, true);
+
+    output.clear();
+    result = channel->consumeIpmiSerialPacket(input1, output);
+    ASSERT_EQ(result, false);
+    result = channel->consumeIpmiSerialPacket(input2, output);
+    ASSERT_EQ(result, true);
+}
+
+} // namespace serialbridge