Add IPMI interface for CustomAccel service

Change-Id: I28a8976e382b457233ac521e9ab71f75abe029d1
Signed-off-by: Steve Foreman <foremans@google.com>
diff --git a/.gitignore b/.gitignore
index 327ea97..fe2e2c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 /build*/
 /subprojects/*/
+/.cache/clangd/index/
+/.vscode/
diff --git a/README.md b/README.md
index eb79fd5..70fa020 100644
--- a/README.md
+++ b/README.md
@@ -235,3 +235,168 @@
 |Byte(s) |Value  |Data
 |--------|-------|----
 |0x00|0x0A|Subcommand
+
+### AccelOobDeviceCount - SubCommand 0x0B
+
+Query the number of available devices from the google-accel-oob service.
+
+If not enough data is proveded, `IPMI_CC_REQ_DATA_LEN_INVALID` is returned.
+
+Request
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0B|Subcommand
+
+Response
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0B|Subcommand
+|0x01..0x04| |Number of devices available
+
+### AccelOobDeviceName - SubCommand 0x0C
+
+Query the name of a single device from the google-accel-oob service.
+
+This name is used as the identifier for the AccelOobRead and AccelOobWrite
+commands.
+
+Index values start at zero and go up to (but don't include) the device count.
+
+The name of the device is exactly as it appears in DBus, except for the common
+"/com/google/customAccel/" prefix. This prefix is removed to reduce the size of
+the IPMI packet.
+
+DBus requires all element names to be non-empty strings of ASCII characters
+"[A-Z][a-z][0-9]_", seperated by ASCII '/'. Therefore, all device names will be
+valid ASCII strings (1 byte/character).
+
+For convenience, the name string is followed by a single 0x00 (NULL terminator)
+byte which is not included as part of the length.
+
+The length field (byte 5) is the number of bytes in the name string (not
+including the trailing NULL terminator byte).
+
+The maximum length for any name is 43 bytes (not including the trailing NULL).
+
+If a name is longer than 43 bytes, `IPMI_CC_REQ_DATA_TRUNCATED` is returned.
+These names will not be usable in the rest of the API. Changing the name
+requires code changes to the `managed_acceld` service binary.
+
+If not enough data is proveded, `IPMI_CC_REQ_DATA_LEN_INVALID` is returned.
+
+If a name does not begin with the expected "/com/google/customAccel/" prefix,
+`IPMI_CC_INVALID` is returned. This indicates a change in the DBus API for the
+google-accel-oob service that requires a matching code change in the handler.
+
+Request
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0C|Subcommand
+|0x05| |Length of the name
+|0x06..n| |Name of the device
+
+Response
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0C|Subcommand
+|0x01..0x04| |Index of the device
+|0x05| |Length of the name
+|0x06..n| |Name of the device
+
+### AccelOobRead - SubCommand 0x0D
+
+Read a PCIe CSR from a device.
+
+Length is the length of the name, in bytes.
+
+The device name gets prepended with "/com/google/customAccel/" and sent to DBus.
+This string must **NOT** have a trailing NULL terminator.
+
+The token is an arbitrary byte that gets echoed back in the reply; it is not
+interpreted by the service at all. This is used to disambiguate identical
+requests so clients can check for lost transactions.
+
+Address is the 64b PCIe address to read from.
+
+Number of bytes is the size of the read, in bytes (max 8). The value is subject
+to hardware limitations (both PCIe and ASIC), so it will generally be 1, 2, 4,
+or 8.
+
+The register data is always returned in 8 bytes (uint64) in little Endian order.
+If fewer than than 8 bytes are read, the MSBs are padded with 0s.
+
+On success, the response ends with the data read as a single uint64.
+
+If the number of bytes requested would not fit in a single IPMI payload,
+`IPMI_CC_REQUESTED_TOO_MANY_BYTES` is returned.
+
+If not enough data is proveded, `IPMI_CC_REQ_DATA_LEN_INVALID` is returned.
+
+Request
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0D|Subcommand
+|0x01| |Number of bytes in the device name
+|0x02..n| |Name of the device (from `AccelOobDeviceName`)
+|n+1| |Token
+|n+2..n+10| |Address
+|n+11| |Number of bytes
+
+Response
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0D|Subcommand
+|0x01| |Number of bytes in the device name
+|0x02..n| |Name of the device (no trailing NULL)
+|n+1| |Token
+|n+2..n+10| |Address
+|n+11| |Number of bytes
+|n+12..n+20| |Data
+
+### AccelOobWrite - SubCommand 0x0E
+
+Write a PCIe CSR from a device.
+
+All parameters are identical to AccelOobRead (above). The only difference is
+the register data becomes an input parameter (in the Request) instead of an
+output value (in the Response).
+
+As with read, the register data must be 8 bytes (uint64) in little Endian order.
+If fewer than 8 bytes will be written, only the LSBs will be read and the the
+MSBs will be ignored.
+
+All fields returned in the Response are simply a copy of the Request.
+
+On success, `IPMI_CC_OK` is returned.
+
+If not enough data is proveded, `IPMI_CC_REQ_DATA_LEN_INVALID` is returned.
+
+Request
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0D|Subcommand
+|0x01| |Number of bytes in the device name
+|0x02..n| |Name of the device (from `AccelOobDeviceName`)
+|n+1| |Token
+|n+2..n+10| |Address
+|n+11| |Number of bytes
+|n+12..n+20| |Data
+
+Response
+
+|Byte(s) |Value  |Data
+|--------|-------|----
+|0x00|0x0D|Subcommand
+|0x01| |Number of bytes in the device name
+|0x02..n| |Name of the device (no trailing NULL)
+|n+1| |Token
+|n+2..n+10| |Address
+|n+11| |Number of bytes
+|n+12..n+20| |Data
diff --git a/commands.hpp b/commands.hpp
index 87333c5..8471185 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -43,6 +43,14 @@
     SysGetFlashSize = 9,
     // The Sys Host Power Off with disabled fallback watchdog
     SysHostPowerOff = 10,
+    // Google CustomAccel service - get the number of devices available
+    SysAccelOobDeviceCount = 11,
+    // Google CustomAccel service - get the name of a single device
+    SysAccelOobDeviceName = 12,
+    // Google CustomAccel service - read from a device
+    SysAccelOobRead = 13,
+    // Google CustomAccel service - write to a device
+    SysAccelOobWrite = 14,
 };
 
 } // namespace ipmi
diff --git a/google_accel_oob.cpp b/google_accel_oob.cpp
new file mode 100644
index 0000000..e0fa615
--- /dev/null
+++ b/google_accel_oob.cpp
@@ -0,0 +1,282 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "google_accel_oob.hpp"
+
+#include "commands.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <sdbusplus/bus.hpp>
+#include <span>
+#include <string>
+#include <vector>
+
+namespace google
+{
+namespace ipmi
+{
+
+#ifndef MAX_IPMI_BUFFER
+#define MAX_IPMI_BUFFER 64
+#endif
+
+// token + address(8) + num_bytes + data(8) + len + NULL
+constexpr size_t MAX_NAME_SIZE = MAX_IPMI_BUFFER - 1 - 8 - 1 - 8 - 1 - 1;
+
+Resp accelOobDeviceCount(std::span<const uint8_t> data,
+                         HandlerInterface* handler)
+{
+    struct Request
+    {
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint32_t count;
+    } __attribute__((packed));
+
+    if (data.size_bytes() < sizeof(Request))
+    {
+        std::fprintf(stderr, "AccelOob DeviceCount command too small: %zu\n",
+                     data.size_bytes());
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    if (data.size_bytes() + sizeof(Reply) > MAX_IPMI_BUFFER)
+    {
+        std::fprintf(stderr,
+                     "AccelOob DeviceCount command too large for reply buffer: "
+                     "command=%zuB, payload=%zuB, max=%dB\n",
+                     data.size_bytes(), sizeof(Reply), MAX_IPMI_BUFFER);
+        return ::ipmi::responseReqDataLenExceeded();
+    }
+
+    uint32_t count = handler->accelOobDeviceCount();
+
+    std::vector<uint8_t> replyBuf(sizeof(Reply));
+    auto* reply = reinterpret_cast<Reply*>(replyBuf.data());
+    reply->count = count;
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysAccelOobDeviceCount,
+                                   replyBuf);
+}
+
+Resp accelOobDeviceName(std::span<const uint8_t> data,
+                        HandlerInterface* handler)
+{
+    struct Request
+    {
+        uint32_t index;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint8_t nameLength;
+        char name[MAX_NAME_SIZE];
+    } __attribute__((packed));
+
+    if (data.size_bytes() < sizeof(Request))
+    {
+        std::fprintf(stderr, "AccelOob DeviceName command too small: %zu\n",
+                     data.size_bytes());
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    if (data.size_bytes() + sizeof(Reply) > MAX_IPMI_BUFFER)
+    {
+        std::fprintf(stderr,
+                     "AccelOob DeviceName command too large for reply buffer: "
+                     "command=%zuB, payload=%zuB, max=%dB\n",
+                     data.size_bytes(), sizeof(Reply), MAX_IPMI_BUFFER);
+        return ::ipmi::responseReqDataLenExceeded();
+    }
+
+    auto* req = reinterpret_cast<const Request*>(data.data());
+    std::string name = handler->accelOobDeviceName(req->index);
+
+    if (name.size() > MAX_NAME_SIZE)
+    {
+        std::fprintf(stderr,
+                     "AccelOob: name was too long. "
+                     "'%s' len must be <= %zu\n",
+                     name.c_str(), MAX_NAME_SIZE);
+        return ::ipmi::responseReqDataTruncated();
+    }
+
+    std::vector<uint8_t> replyBuf(data.size_bytes() + sizeof(Reply));
+    std::copy(data.begin(), data.end(), replyBuf.data());
+    auto* reply = reinterpret_cast<Reply*>(replyBuf.data() + data.size_bytes());
+    reply->nameLength = name.length();
+    memcpy(reply->name, name.c_str(), reply->nameLength + 1);
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysAccelOobDeviceName,
+                                   replyBuf);
+}
+
+namespace
+{
+
+struct NameHeader
+{
+    uint8_t nameLength;
+    char name[MAX_NAME_SIZE];
+} __attribute__((packed));
+
+// Reads the variable-length name from reqBuf and outputs the name and a pointer
+// to the payload (next byte after name).
+//
+// Returns: =0: success.
+//          >0: if dataLen is too small, returns the minimum buffers size.
+//
+// Params:
+//    [in]  reqBuf      - the request buffer
+//    [in]  dataLen     - the length of reqBuf, in bytes
+//    [in]  payloadSize - the size of the expected payload
+//    [out] name        - the name string
+//    [out] payload     - pointer into reqBuf just after name
+size_t ReadNameHeader(const uint8_t* reqBuf, size_t dataLen, size_t payloadSize,
+                      std::string* name, const uint8_t** payload)
+{
+    constexpr size_t kNameHeaderSize = sizeof(NameHeader) - MAX_NAME_SIZE;
+
+    auto* req_header = reinterpret_cast<const NameHeader*>(reqBuf);
+
+    size_t minDataLen = kNameHeaderSize + payloadSize + req_header->nameLength;
+    if (dataLen < minDataLen)
+    {
+        return minDataLen;
+    }
+
+    if (name)
+    {
+        *name = std::string(req_header->name, req_header->nameLength);
+    }
+    if (payload)
+    {
+        *payload = reqBuf + kNameHeaderSize + req_header->nameLength;
+    }
+    return 0;
+}
+
+} // namespace
+
+Resp accelOobRead(std::span<const uint8_t> data, HandlerInterface* handler)
+{
+    struct Request
+    {
+        // Variable length header, handled by ReadNameHeader
+        // uint8_t  nameLength;  // <= MAX_NAME_SIZE
+        // char     name[nameLength];
+
+        // Additional arguments
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint64_t data;
+    } __attribute__((packed));
+
+    std::fprintf(stderr,
+                 "AccelOob Read command sizes: "
+                 "command=%zuB, payload=%zuB, max=%dB\n",
+                 data.size_bytes(), sizeof(Reply), MAX_IPMI_BUFFER);
+
+    std::string name;
+    const uint8_t* payload;
+
+    size_t min_size = ReadNameHeader(data.data(), data.size_bytes(),
+                                     sizeof(Request), &name, &payload);
+    if (min_size != 0)
+    {
+        std::fprintf(stderr, "AccelOob Read command too small: %zuB < %zuB\n",
+                     data.size_bytes(), min_size);
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    if (data.size_bytes() + sizeof(Reply) > MAX_IPMI_BUFFER)
+    {
+        std::fprintf(stderr,
+                     "AccelOob Read command too large for reply buffer: "
+                     "command=%zuB, payload=%zuB, max=%dB\n",
+                     data.size_bytes(), sizeof(Reply), MAX_IPMI_BUFFER);
+        return ::ipmi::responseReqDataLenExceeded();
+    }
+
+    auto req = reinterpret_cast<const Request*>(payload);
+    uint64_t r = handler->accelOobRead(name, req->address, req->num_bytes);
+
+    std::vector<uint8_t> replyBuf(data.size_bytes() + sizeof(Reply));
+    std::copy(data.begin(), data.end(), replyBuf.data());
+    auto* reply = reinterpret_cast<Reply*>(replyBuf.data() + data.size_bytes());
+    reply->data = r;
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysAccelOobRead, replyBuf);
+}
+
+Resp accelOobWrite(std::span<const uint8_t> data, HandlerInterface* handler)
+{
+    struct Request
+    {
+        // Variable length header, handled by ReadNameHeader
+        // uint8_t  nameLength;  // <= MAX_NAME_SIZE
+        // char     name[nameLength];
+
+        // Additional arguments
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+        uint64_t data;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        // Empty
+    } __attribute__((packed));
+
+    std::string name{};
+    const uint8_t* payload;
+
+    size_t min_size = ReadNameHeader(data.data(), data.size_bytes(),
+                                     sizeof(Request), &name, &payload);
+    if (min_size != 0)
+    {
+        std::fprintf(stderr, "AccelOob Write command too small: %zuB < %zuB\n",
+                     data.size_bytes(), min_size);
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    if (data.size_bytes() + sizeof(Reply) > MAX_IPMI_BUFFER)
+    {
+        std::fprintf(stderr,
+                     "AccelOob Write command too large for reply buffer: "
+                     "command=%zuB, payload=%zuB, max=%dB\n",
+                     data.size_bytes(), sizeof(Reply), MAX_IPMI_BUFFER);
+        return ::ipmi::responseReqDataLenExceeded();
+    }
+
+    auto req = reinterpret_cast<const Request*>(payload);
+    handler->accelOobWrite(name, req->address, req->num_bytes, req->data);
+
+    std::vector<uint8_t> replyBuf(data.size_bytes() + sizeof(Reply));
+    std::copy(data.begin(), data.end(), replyBuf.data());
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysAccelOobWrite, replyBuf);
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/google_accel_oob.hpp b/google_accel_oob.hpp
new file mode 100644
index 0000000..2624f2b
--- /dev/null
+++ b/google_accel_oob.hpp
@@ -0,0 +1,40 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "handler.hpp"
+
+#include <ipmid/api.h>
+
+#include <span>
+
+namespace google
+{
+namespace ipmi
+{
+
+//  Handle the Accel OOB device count command
+Resp accelOobDeviceCount(std::span<const uint8_t> data,
+                         HandlerInterface* handler);
+
+Resp accelOobDeviceName(std::span<const uint8_t> data,
+                        HandlerInterface* handler);
+
+Resp accelOobRead(std::span<const uint8_t> data, HandlerInterface* handler);
+
+Resp accelOobWrite(std::span<const uint8_t> data, HandlerInterface* handler);
+
+} // namespace ipmi
+} // namespace google
diff --git a/handler.cpp b/handler.cpp
index c716af6..8ec7863 100644
--- a/handler.cpp
+++ b/handler.cpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
 #include <string>
 #include <string_view>
 #include <tuple>
+#include <variant>
 #include <xyz/openbmc_project/Common/error.hpp>
 
 #ifndef NCSI_IF_NAME
@@ -392,5 +393,211 @@
     return _pcie_i2c_map[entry];
 }
 
+namespace
+{
+
+static constexpr std::string_view ACCEL_OOB_ROOT = "/com/google/customAccel/";
+static constexpr char ACCEL_OOB_SERVICE[] = "com.google.custom_accel";
+static constexpr char ACCEL_OOB_INTERFACE[] = "com.google.custom_accel.BAR";
+
+// C type for "a{oa{sa{sv}}}" from DBus.ObjectManager::GetManagedObjects()
+using AnyType = std::variant<std::string, uint8_t, uint32_t, uint64_t>;
+using AnyTypeList = std::vector<std::pair<std::string, AnyType>>;
+using NamedArrayOfAnyTypeLists =
+    std::vector<std::pair<std::string, AnyTypeList>>;
+using ArrayOfObjectPathsAndTieredAnyTypeLists = std::vector<
+    std::pair<sdbusplus::message::object_path, NamedArrayOfAnyTypeLists>>;
+
+} // namespace
+
+sdbusplus::bus::bus Handler::accelOobGetDbus() const
+{
+    return sdbusplus::bus::new_default();
+}
+
+uint32_t Handler::accelOobDeviceCount() const
+{
+    ArrayOfObjectPathsAndTieredAnyTypeLists data;
+
+    try
+    {
+        auto bus = accelOobGetDbus();
+        auto method = bus.new_method_call(ACCEL_OOB_SERVICE, "/",
+                                          "org.freedesktop.DBus.ObjectManager",
+                                          "GetManagedObjects");
+        bus.call(method).read(data);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>(
+            "Failed to call GetManagedObjects on com.google.custom_accel",
+            entry("WHAT=%s", ex.what()));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    return data.size();
+}
+
+std::string Handler::accelOobDeviceName(size_t index) const
+{
+    ArrayOfObjectPathsAndTieredAnyTypeLists data;
+
+    try
+    {
+        auto bus = accelOobGetDbus();
+        auto method = bus.new_method_call(ACCEL_OOB_SERVICE, "/",
+                                          "org.freedesktop.DBus.ObjectManager",
+                                          "GetManagedObjects");
+        bus.call(method).read(data);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>(
+            "Failed to call GetManagedObjects on com.google.custom_accel",
+            entry("WHAT=%s", ex.what()));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    if (index >= data.size())
+    {
+        log<level::WARNING>(
+            "Requested index is larger than the number of entries.",
+            entry("INDEX=%zu", index), entry("NUM_NAMES=%zu", data.size()));
+        throw IpmiException(IPMI_CC_PARM_OUT_OF_RANGE);
+    }
+
+    std::string_view name(data[index].first.str);
+    if (!name.starts_with(ACCEL_OOB_ROOT))
+    {
+        throw IpmiException(IPMI_CC_INVALID);
+    }
+    name.remove_prefix(ACCEL_OOB_ROOT.length());
+    return std::string(name);
+}
+
+uint64_t Handler::accelOobRead(std::string_view name, uint64_t address,
+                               uint8_t num_bytes) const
+{
+    static constexpr char ACCEL_OOB_METHOD[] = "Read";
+
+    std::string object_name(ACCEL_OOB_ROOT);
+    object_name.append(name);
+
+    auto bus = accelOobGetDbus();
+    auto method = bus.new_method_call(ACCEL_OOB_SERVICE, object_name.c_str(),
+                                      ACCEL_OOB_INTERFACE, ACCEL_OOB_METHOD);
+    method.append(address, static_cast<uint64_t>(num_bytes));
+
+    std::vector<uint8_t> bytes;
+
+    try
+    {
+        bus.call(method).read(bytes);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>("Failed to call Read on com.google.custom_accel",
+                        entry("WHAT=%s", ex.what()),
+                        entry("DBUS_SERVICE=%s", ACCEL_OOB_SERVICE),
+                        entry("DBUS_OBJECT=%s", object_name.c_str()),
+                        entry("DBUS_INTERFACE=%s", ACCEL_OOB_INTERFACE),
+                        entry("DBUS_METHOD=%s", ACCEL_OOB_METHOD),
+                        entry("DBUS_ARG_ADDRESS=%016llx", address),
+                        entry("DBUS_ARG_NUM_BYTES=%zu", (size_t)num_bytes));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    if (bytes.size() < num_bytes)
+    {
+        log<level::ERR>(
+            "Call to Read on com.google.custom_accel didn't return the expected"
+            " number of bytes.",
+            entry("DBUS_SERVICE=%s", ACCEL_OOB_SERVICE),
+            entry("DBUS_OBJECT=%s", object_name.c_str()),
+            entry("DBUS_INTERFACE=%s", ACCEL_OOB_INTERFACE),
+            entry("DBUS_METHOD=%s", ACCEL_OOB_METHOD),
+            entry("DBUS_ARG_ADDRESS=%016llx", address),
+            entry("DBUS_ARG_NUM_BYTES=%zu", (size_t)num_bytes),
+            entry("DBUS_RETURN_SIZE=%zu", bytes.size()));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+
+    if (bytes.size() > sizeof(uint64_t))
+    {
+        log<level::ERR>(
+            "Call to Read on com.google.custom_accel returned more than 8B.",
+            entry("DBUS_SERVICE=%s", ACCEL_OOB_SERVICE),
+            entry("DBUS_OBJECT=%s", object_name.c_str()),
+            entry("DBUS_INTERFACE=%s", ACCEL_OOB_INTERFACE),
+            entry("DBUS_METHOD=%s", ACCEL_OOB_METHOD),
+            entry("DBUS_ARG_ADDRESS=%016llx", address),
+            entry("DBUS_ARG_NUM_BYTES=%zu", (size_t)num_bytes),
+            entry("DBUS_RETURN_SIZE=%zu", bytes.size()));
+        throw IpmiException(IPMI_CC_REQ_DATA_TRUNCATED);
+    }
+
+    uint64_t data = 0;
+    for (size_t i = 0; i < num_bytes; ++i)
+    {
+        data = (data << 8) | bytes[i];
+    }
+
+    return data;
+}
+
+void Handler::accelOobWrite(std::string_view name, uint64_t address,
+                            uint8_t num_bytes, uint64_t data) const
+{
+    static constexpr std::string_view ACCEL_OOB_METHOD = "Write";
+
+    std::string object_name(ACCEL_OOB_ROOT);
+    object_name.append(name);
+
+    if (num_bytes > sizeof(data))
+    {
+        log<level::ERR>(
+            "Call to Write on com.google.custom_accel requested more than 8B.",
+            entry("DBUS_SERVICE=%s", ACCEL_OOB_SERVICE),
+            entry("DBUS_OBJECT=%s", object_name.c_str()),
+            entry("DBUS_INTERFACE=%s", ACCEL_OOB_INTERFACE),
+            entry("DBUS_METHOD=%s", ACCEL_OOB_METHOD.data()),
+            entry("DBUS_ARG_ADDRESS=%016llx", address),
+            entry("DBUS_ARG_NUM_BYTES=%zu", (size_t)num_bytes),
+            entry("DBUS_ARG_DATA=%016llx", data));
+        throw IpmiException(IPMI_CC_PARM_OUT_OF_RANGE);
+    }
+
+    std::vector<uint8_t> bytes;
+    bytes.reserve(num_bytes);
+    for (size_t i = 0; i < num_bytes; ++i)
+    {
+        bytes.emplace_back(data & 0xff);
+        data >>= 8;
+    }
+
+    try
+    {
+        auto bus = accelOobGetDbus();
+        auto method =
+            bus.new_method_call(ACCEL_OOB_SERVICE, object_name.c_str(),
+                                ACCEL_OOB_INTERFACE, ACCEL_OOB_METHOD.data());
+        method.append(address, bytes);
+        bus.call_noreply(method);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>("Failed to call Write on com.google.custom_accel",
+                        entry("WHAT=%s", ex.what()),
+                        entry("DBUS_SERVICE=%s", ACCEL_OOB_SERVICE),
+                        entry("DBUS_OBJECT=%s", object_name.c_str()),
+                        entry("DBUS_INTERFACE=%s", ACCEL_OOB_INTERFACE),
+                        entry("DBUS_METHOD=%s", ACCEL_OOB_METHOD.data()),
+                        entry("DBUS_ARG_ADDRESS=%016llx", address),
+                        entry("DBUS_ARG_NUM_BYTES=%zu", (size_t)num_bytes),
+                        entry("DBUS_ARG_DATA=%016llx", data));
+        throw IpmiException(IPMI_CC_UNSPECIFIED_ERROR);
+    }
+}
+
 } // namespace ipmi
 } // namespace google
diff --git a/handler.hpp b/handler.hpp
index 26950e9..8c36c52 100644
--- a/handler.hpp
+++ b/handler.hpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
 #include <map>
 #include <span>
 #include <string>
+#include <string_view>
 #include <tuple>
 #include <vector>
 
@@ -136,6 +137,56 @@
      * @throw IpmiException on failure.
      */
     virtual void hostPowerOffDelay(std::uint32_t delay) const = 0;
+
+    /**
+     * Return the number of devices from the CustomAccel service.
+     *
+     * @return the number of devices.
+     * @throw IpmiException on failure.
+     */
+    virtual uint32_t accelOobDeviceCount() const = 0;
+
+    /**
+     * Return the name of a single device from the CustomAccel service.
+     *
+     * Valid indexes start at 0 and go up to (but don't include) the number of
+     * devices. The number of devices can be queried with accelOobDeviceCount.
+     *
+     * @param[in] index - the index of the device, starting at 0.
+     * @return the name of the device.
+     * @throw IpmiException on failure.
+     */
+    virtual std::string accelOobDeviceName(size_t index) const = 0;
+
+    /**
+     * Read from a single CustomAccel service device.
+     *
+     * Valid device names can be queried with accelOobDeviceName.
+     * If num_bytes < 8, all unused MSBs are padded with 0s.
+     *
+     * @param[in] name - the name of the device (from DeviceName).
+     * @param[in] address - the address to read from.
+     * @param[in] num_bytes - the size of the read, in bytes.
+     * @return the data read, with 0s padding any unused MSBs.
+     * @throw IpmiException on failure.
+     */
+    virtual uint64_t accelOobRead(std::string_view name, uint64_t address,
+                                  uint8_t num_bytes) const = 0;
+
+    /**
+     * Write to a single CustomAccel service device.
+     *
+     * Valid device names can be queried with accelOobDeviceName.
+     * If num_bytes < 8, all unused MSBs are ignored.
+     *
+     * @param[in] name - the name of the device (from DeviceName).
+     * @param[in] address - the address to read from.
+     * @param[in] num_bytes - the size of the read, in bytes.
+     * @param[in] data - the data to write.
+     * @throw IpmiException on failure.
+     */
+    virtual void accelOobWrite(std::string_view name, uint64_t address,
+                               uint8_t num_bytes, uint64_t data) const = 0;
 };
 
 } // namespace ipmi
diff --git a/handler_impl.hpp b/handler_impl.hpp
index 88a901b..f46e79c 100644
--- a/handler_impl.hpp
+++ b/handler_impl.hpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -19,7 +19,9 @@
 #include <cstdint>
 #include <map>
 #include <nlohmann/json.hpp>
+#include <sdbusplus/bus.hpp>
 #include <string>
+#include <string_view>
 #include <tuple>
 #include <vector>
 
@@ -53,6 +55,17 @@
     std::tuple<std::uint32_t, std::string>
         getI2cEntry(unsigned int entry) const override;
 
+    uint32_t accelOobDeviceCount() const override;
+    std::string accelOobDeviceName(size_t index) const override;
+    uint64_t accelOobRead(std::string_view name, uint64_t address,
+                          uint8_t num_bytes) const override;
+    void accelOobWrite(std::string_view name, uint64_t address,
+                       uint8_t num_bytes, uint64_t data) const override;
+
+  protected:
+    // Exposed for dependency injection
+    virtual sdbusplus::bus::bus accelOobGetDbus() const;
+
   private:
     std::string _configFile;
 
diff --git a/ipmi.cpp b/ipmi.cpp
index 1f760f4..715264b 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
 #include "entity_name.hpp"
 #include "eth.hpp"
 #include "flash_size.hpp"
+#include "google_accel_oob.hpp"
 #include "handler.hpp"
 #include "host_power_off.hpp"
 #include "machine_name.hpp"
@@ -67,6 +68,14 @@
             return getFlashSize(data, handler);
         case SysHostPowerOff:
             return hostPowerOff(data, handler);
+        case SysAccelOobDeviceCount:
+            return accelOobDeviceCount(data, handler);
+        case SysAccelOobDeviceName:
+            return accelOobDeviceName(data, handler);
+        case SysAccelOobRead:
+            return accelOobRead(data, handler);
+        case SysAccelOobWrite:
+            return accelOobWrite(data, handler);
         default:
             std::fprintf(stderr, "Invalid subcommand: 0x%x\n", cmd);
             return ::ipmi::responseInvalidCommand();
diff --git a/meson.build b/meson.build
index d6f5c96..ee34798 100644
--- a/meson.build
+++ b/meson.build
@@ -40,6 +40,7 @@
   'ipmi.cpp',
   'machine_name.cpp',
   'pcie_i2c.cpp',
+  'google_accel_oob.cpp',
   'psu.cpp',
   'util.cpp',
   implicit_include_directories: false,
diff --git a/test/google_accel_oob_unittest.cpp b/test/google_accel_oob_unittest.cpp
new file mode 100644
index 0000000..163b602
--- /dev/null
+++ b/test/google_accel_oob_unittest.cpp
@@ -0,0 +1,239 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "commands.hpp"
+#include "errors.hpp"
+#include "google_accel_oob.hpp"
+#include "handler_mock.hpp"
+
+#include <ipmid/api.h>
+
+#include <gtest/gtest.h>
+
+namespace google
+{
+namespace ipmi
+{
+
+using ::testing::Return;
+
+TEST(GoogleAccelOobTest, DeviceCount_Success)
+{
+    ::testing::StrictMock<HandlerMock> h;
+
+    uint8_t reqBuf[1]; // Could be 0, but zero-length arrays are an extension
+
+    struct Reply
+    {
+        uint32_t count;
+    } __attribute__((packed));
+
+    constexpr uint32_t kTestDeviceCount = 2;
+
+    EXPECT_CALL(h, accelOobDeviceCount()).WillOnce(Return(kTestDeviceCount));
+
+    Resp r = accelOobDeviceCount(reqBuf, &h);
+
+    const auto response = std::get<0>(r);
+    EXPECT_EQ(response, IPMI_CC_OK);
+
+    const auto payload = std::get<1>(r);
+    ASSERT_EQ(payload.has_value(), true);
+    const auto payload_tuple = payload.value();
+    const auto reply_cmd = std::get<0>(payload_tuple);
+    EXPECT_EQ(reply_cmd, SysAccelOobDeviceCount);
+    const auto reply_buff = std::get<1>(payload_tuple);
+    ASSERT_EQ(reply_buff.size(), sizeof(Reply));
+
+    auto* reply = reinterpret_cast<const Reply*>(reply_buff.data());
+    EXPECT_EQ(reply->count, kTestDeviceCount);
+}
+
+TEST(GoogleAccelOobTest, DeviceName_Success)
+{
+    ::testing::StrictMock<HandlerMock> h;
+
+    struct Request
+    {
+        uint32_t index;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint32_t index;
+        uint8_t length;
+        char name[1];
+    } __attribute__((packed));
+
+    constexpr uint32_t kTestDeviceIndex = 0;
+    const std::string kTestDeviceName("testDeviceName");
+
+    EXPECT_CALL(h, accelOobDeviceName(kTestDeviceIndex))
+        .WillOnce(Return(kTestDeviceName));
+
+    Request reqBuf{kTestDeviceIndex};
+    Resp r = accelOobDeviceName(
+        std::span(reinterpret_cast<const uint8_t*>(&reqBuf), sizeof(Request)),
+        &h);
+
+    const auto response = std::get<0>(r);
+    EXPECT_EQ(response, IPMI_CC_OK);
+
+    const auto payload = std::get<1>(r);
+    ASSERT_EQ(payload.has_value(), true);
+    const auto payload_tuple = payload.value();
+    const auto reply_cmd = std::get<0>(payload_tuple);
+    EXPECT_EQ(reply_cmd, SysAccelOobDeviceName);
+    const auto reply_buff = std::get<1>(payload_tuple);
+    ASSERT_GE(reply_buff.size(), sizeof(Reply));
+
+    auto* reply = reinterpret_cast<const Reply*>(reply_buff.data());
+    EXPECT_EQ(reply->index, kTestDeviceIndex);
+    EXPECT_EQ(reply->length, kTestDeviceName.length());
+    EXPECT_STREQ(reply->name, kTestDeviceName.c_str());
+}
+
+TEST(GoogleAccelOobTest, Read_Success)
+{
+    ::testing::StrictMock<HandlerMock> h;
+
+    constexpr char kTestDeviceName[] = "testDeviceName";
+    constexpr uint8_t kTestDeviceNameLength =
+        (sizeof(kTestDeviceName) / sizeof(*kTestDeviceName)) - 1;
+    constexpr uint8_t kTestToken = 0xAB;
+    constexpr uint64_t kTestAddress = 0;
+    constexpr uint8_t kTestReadSize = 8;
+    constexpr uint64_t kTestData = 0x12345678;
+
+    struct Request
+    {
+        uint8_t nameLength;
+        char name[kTestDeviceNameLength];
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint8_t nameLength;
+        char name[kTestDeviceNameLength];
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+        uint64_t data;
+    } __attribute__((packed));
+
+    const std::string_view kTestDeviceNameStr(kTestDeviceName,
+                                              kTestDeviceNameLength);
+    EXPECT_CALL(h,
+                accelOobRead(kTestDeviceNameStr, kTestAddress, kTestReadSize))
+        .WillOnce(Return(kTestData));
+
+    Request reqBuf{kTestDeviceNameLength, "", kTestToken, kTestAddress,
+                   kTestReadSize};
+    memcpy(reqBuf.name, kTestDeviceName, kTestDeviceNameLength);
+    Resp r = accelOobRead(
+        std::span(reinterpret_cast<const uint8_t*>(&reqBuf), sizeof(Request)),
+        &h);
+
+    const auto response = std::get<0>(r);
+    EXPECT_EQ(response, IPMI_CC_OK);
+
+    const auto payload = std::get<1>(r);
+    ASSERT_EQ(payload.has_value(), true);
+    const auto payload_tuple = payload.value();
+    const auto reply_cmd = std::get<0>(payload_tuple);
+    EXPECT_EQ(reply_cmd, SysAccelOobRead);
+    const auto reply_buff = std::get<1>(payload_tuple);
+    ASSERT_GE(reply_buff.size(), sizeof(Reply));
+
+    auto* reply = reinterpret_cast<const Reply*>(reply_buff.data());
+    EXPECT_EQ(reply->nameLength, kTestDeviceNameLength);
+    EXPECT_EQ(std::string_view(reply->name, reply->nameLength),
+              kTestDeviceNameStr);
+    EXPECT_EQ(reply->token, kTestToken);
+    EXPECT_EQ(reply->address, kTestAddress);
+    EXPECT_EQ(reply->num_bytes, kTestReadSize);
+    EXPECT_EQ(reply->data, kTestData);
+}
+
+TEST(GoogleAccelOobTest, Write_Success)
+{
+    ::testing::StrictMock<HandlerMock> h;
+
+    constexpr char kTestDeviceName[] = "testDeviceName";
+    constexpr uint8_t kTestDeviceNameLength =
+        (sizeof(kTestDeviceName) / sizeof(*kTestDeviceName)) - 1;
+    constexpr uint8_t kTestToken = 0xAB;
+    constexpr uint64_t kTestAddress = 0;
+    constexpr uint8_t kTestWriteSize = 8;
+    constexpr uint64_t kTestData = 0x12345678;
+
+    struct Request
+    {
+        uint8_t nameLength;
+        char name[kTestDeviceNameLength];
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+        uint64_t data;
+    } __attribute__((packed));
+
+    struct Reply
+    {
+        uint8_t nameLength;
+        char name[kTestDeviceNameLength];
+        uint8_t token;
+        uint64_t address;
+        uint8_t num_bytes;
+        uint64_t data;
+    } __attribute__((packed));
+
+    const std::string_view kTestDeviceNameStr(kTestDeviceName,
+                                              kTestDeviceNameLength);
+    EXPECT_CALL(h, accelOobWrite(kTestDeviceNameStr, kTestAddress,
+                                 kTestWriteSize, kTestData))
+        .WillOnce(Return());
+
+    Request reqBuf{kTestDeviceNameLength, "",       kTestToken, kTestAddress,
+                   kTestWriteSize,        kTestData};
+    memcpy(reqBuf.name, kTestDeviceName, kTestDeviceNameLength);
+    Resp r = accelOobWrite(
+        std::span(reinterpret_cast<const uint8_t*>(&reqBuf), sizeof(Request)),
+        &h);
+
+    const auto response = std::get<0>(r);
+    EXPECT_EQ(response, IPMI_CC_OK);
+
+    const auto payload = std::get<1>(r);
+    ASSERT_EQ(payload.has_value(), true);
+    const auto payload_tuple = payload.value();
+    const auto reply_cmd = std::get<0>(payload_tuple);
+    EXPECT_EQ(reply_cmd, SysAccelOobWrite);
+    const auto reply_buff = std::get<1>(payload_tuple);
+    ASSERT_GE(reply_buff.size(), sizeof(Reply));
+
+    auto* reply = reinterpret_cast<const Reply*>(reply_buff.data());
+    EXPECT_EQ(reply->nameLength, kTestDeviceNameLength);
+    EXPECT_EQ(std::string_view(reply->name, reply->nameLength),
+              kTestDeviceNameStr);
+    EXPECT_EQ(reply->token, kTestToken);
+    EXPECT_EQ(reply->address, kTestAddress);
+    EXPECT_EQ(reply->num_bytes, kTestWriteSize);
+    EXPECT_EQ(reply->data, kTestData);
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/test/handler_mock.hpp b/test/handler_mock.hpp
index d393bde..2fb8e2a 100644
--- a/test/handler_mock.hpp
+++ b/test/handler_mock.hpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <string>
+#include <string_view>
 #include <tuple>
 
 #include <gmock/gmock.h>
@@ -53,6 +54,14 @@
     MOCK_METHOD((std::tuple<std::uint32_t, std::string>), getI2cEntry,
                 (unsigned int), (const, override));
     MOCK_METHOD(void, hostPowerOffDelay, (std::uint32_t), (const, override));
+
+    MOCK_METHOD(uint32_t, accelOobDeviceCount, (), (const, override));
+    MOCK_METHOD(std::string, accelOobDeviceName, (size_t), (const, override));
+    MOCK_METHOD(uint64_t, accelOobRead, (std::string_view, uint64_t, uint8_t),
+                (const, override));
+    MOCK_METHOD(void, accelOobWrite,
+                (std::string_view, uint64_t, uint8_t, uint64_t),
+                (const, override));
 };
 
 } // namespace ipmi
diff --git a/test/handler_unittest.cpp b/test/handler_unittest.cpp
index 1dda3ce..418ba46 100644
--- a/test/handler_unittest.cpp
+++ b/test/handler_unittest.cpp
@@ -1,4 +1,4 @@
-// Copyright 2021 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,8 +16,12 @@
 #include "handler.hpp"
 #include "handler_impl.hpp"
 
+#include <systemd/sd-bus.h>
+
 #include <fstream>
 #include <nlohmann/json.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
 #include <string>
 #include <tuple>
 
@@ -106,6 +110,482 @@
     (void)std::remove(testFilename);
 }
 
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::DoAll;
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsNull;
+using ::testing::MatcherCast;
+using ::testing::NotNull;
+using ::testing::Pointee;
+using ::testing::Return;
+using ::testing::ReturnNull;
+using ::testing::SafeMatcherCast;
+using ::testing::SetArgPointee;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+using ::testing::StrNe;
+using ::testing::WithArg;
+
+class MockDbusHandler : public Handler
+{
+  public:
+    MockDbusHandler(sdbusplus::SdBusMock& mock,
+                    const std::string& config = "") :
+        Handler(config),
+        mock_(&mock)
+    {
+    }
+
+  protected:
+    sdbusplus::bus::bus accelOobGetDbus() const override
+    {
+        return sdbusplus::get_mocked_new(
+            const_cast<sdbusplus::SdBusMock*>(mock_));
+    }
+
+  private:
+    sdbusplus::SdBusMock* mock_;
+};
+
+ACTION_TEMPLATE(AssignReadVal, HAS_1_TEMPLATE_PARAMS(typename, T),
+                AND_1_VALUE_PARAMS(val))
+{
+    *static_cast<T*>(arg2) = val;
+}
+
+ACTION_P(TraceDbus, msg)
+{
+    std::fprintf(stderr, "%s\n", msg);
+}
+
+ACTION_P(TraceDbus2, msg)
+{
+    std::fprintf(stderr, "%s(%02x)\n", msg, *static_cast<const uint8_t*>(arg2));
+}
+
+constexpr char object_path[] = "/com/google/customAccel/test/path";
+constexpr char property_grpc[] = "com.google.custom_accel.gRPC";
+constexpr char value_port[] = "Port";
+constexpr uint32_t port = 5000;
+
+constexpr char SD_BUS_TYPE_BYTE_STR[] = {SD_BUS_TYPE_BYTE, '\0'};
+
+// Returns an object that looks like:
+//     "/com/google/customAccel/test/path": {
+//         "com.google.custom_accel.gRPC" : {
+//             "Port" : {
+//                 "type" : "u",
+//                 "data" : 5000
+//             }
+//         }
+//     }
+void ExpectGetManagedObjects(StrictMock<sdbusplus::SdBusMock>& mock,
+                             const char* obj_path = object_path)
+{
+    ::testing::InSequence s;
+
+    // These must be nullptr or sd_bus_message_unref will seg fault.
+    constexpr sd_bus_message* method = nullptr;
+    constexpr sd_bus_message* msg = nullptr;
+
+    EXPECT_CALL(mock, sd_bus_message_new_method_call(
+                          _,         // sd_bus *bus,
+                          NotNull(), // sd_bus_message **m
+                          StrEq("com.google.custom_accel"), StrEq("/"),
+                          StrEq("org.freedesktop.DBus.ObjectManager"),
+                          StrEq("GetManagedObjects")))
+        .WillOnce(DoAll(SetArgPointee<1>(method), Return(0)));
+
+    EXPECT_CALL(mock, sd_bus_call(_,          // sd_bus *bus,
+                                  method,     // sd_bus_message *m
+                                  _,          // uint64_t timeout
+                                  NotNull(),  // sd_bus_error *ret_error
+                                  NotNull())) // sd_bus_message **reply
+        .WillOnce(DoAll(SetArgPointee<3>(SD_BUS_ERROR_NULL),
+                        SetArgPointee<4>(msg), // reply
+                        Return(0)));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY,
+                                                     StrEq("{oa{sa{sv}}}")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(0));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(
+                          msg, SD_BUS_TYPE_DICT_ENTRY, StrEq("oa{sa{sv}}")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_read_basic(msg, SD_BUS_TYPE_OBJECT_PATH,
+                                                NotNull()))
+        .WillOnce(DoAll(AssignReadVal<const char*>(obj_path), Return(1)));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY,
+                                                     StrEq("{sa{sv}}")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(0));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(
+                          msg, SD_BUS_TYPE_DICT_ENTRY, StrEq("sa{sv}")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock,
+                sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, NotNull()))
+        .WillOnce(DoAll(AssignReadVal<const char*>(property_grpc), Return(1)));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY,
+                                                     StrEq("{sv}")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(0));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(
+                          msg, SD_BUS_TYPE_DICT_ENTRY, StrEq("sv")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock,
+                sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, NotNull()))
+        .WillOnce(DoAll(AssignReadVal<const char*>(value_port), Return(1)));
+
+    EXPECT_CALL(
+        mock, sd_bus_message_verify_type(msg, SD_BUS_TYPE_VARIANT, StrNe("u")))
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(
+        mock, sd_bus_message_verify_type(msg, SD_BUS_TYPE_VARIANT, StrEq("u")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_enter_container(msg, SD_BUS_TYPE_VARIANT,
+                                                     StrEq("u")))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock,
+                sd_bus_message_read_basic(msg, SD_BUS_TYPE_UINT32, NotNull()))
+        .WillOnce(DoAll(AssignReadVal<uint32_t>(port), Return(0)));
+
+    EXPECT_CALL(
+        mock, sd_bus_message_verify_type(msg, SD_BUS_TYPE_VARIANT, StrNe("u")))
+        .Times(AnyNumber())
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(mock, sd_bus_message_exit_container(msg))
+        .WillOnce(Return(1))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_exit_container(msg))
+        .WillOnce(Return(1))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_exit_container(msg))
+        .WillOnce(Return(1))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_message_exit_container(msg)).WillOnce(Return(1));
+}
+
+void ExpectSdBusError(StrictMock<sdbusplus::SdBusMock>& mock)
+{
+    EXPECT_CALL(mock, sd_bus_message_new_method_call(
+                          _,         // sd_bus *bus,
+                          NotNull(), // sd_bus_message **m
+                          StrEq("com.google.custom_accel"), StrEq("/"),
+                          StrEq("org.freedesktop.DBus.ObjectManager"),
+                          StrEq("GetManagedObjects")))
+        .WillOnce(Return(-ENOTCONN));
+}
+
+TEST(HandlerTest, accelOobDeviceCount_Success)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectGetManagedObjects(mock);
+    EXPECT_EQ(1, h.accelOobDeviceCount());
+}
+
+TEST(HandlerTest, accelOobDeviceCount_Fail)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectSdBusError(mock);
+    EXPECT_THROW(h.accelOobDeviceCount(), IpmiException);
+}
+
+TEST(HandlerTest, accelOobDeviceName_Success)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectGetManagedObjects(mock);
+    EXPECT_EQ(std::string("test/path"), h.accelOobDeviceName(0));
+}
+
+TEST(HandlerTest, accelOobDeviceName_Fail)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectSdBusError(mock);
+    EXPECT_THROW(h.accelOobDeviceName(0), IpmiException);
+}
+
+TEST(HandlerTest, accelOobDeviceName_OutOfRange)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectGetManagedObjects(mock);
+    EXPECT_THROW(h.accelOobDeviceName(1), IpmiException);
+}
+
+TEST(HandlerTest, accelOobDeviceName_InvalidName)
+{
+    constexpr char bad_object_path[] = "/com/google/customAccel2/bad/path";
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+    ExpectGetManagedObjects(mock, bad_object_path);
+    EXPECT_THROW(h.accelOobDeviceName(0), IpmiException);
+}
+
+constexpr uint8_t NUM_BYTES_RETURNED_EQ_NUM_BYTES = 0xff;
+void ExpectRead(StrictMock<sdbusplus::SdBusMock>& mock, uint64_t address,
+                uint8_t num_bytes, uint64_t data, int sd_bus_call_return_value,
+                uint8_t num_bytes_returned = NUM_BYTES_RETURNED_EQ_NUM_BYTES)
+{
+    ::testing::InSequence s;
+
+    // These must be nullptr or sd_bus_message_unref will seg fault.
+    constexpr sd_bus_message* method = nullptr;
+    constexpr sd_bus_message* msg = nullptr;
+
+    EXPECT_CALL(mock, sd_bus_message_new_method_call(
+                          _,         // sd_bus *bus,
+                          NotNull(), // sd_bus_message **m
+                          StrEq("com.google.custom_accel"),
+                          StrEq("/com/google/customAccel/test/path"),
+                          StrEq("com.google.custom_accel.BAR"), StrEq("Read")))
+        .WillOnce(DoAll(SetArgPointee<1>(method), Return(0)));
+
+    EXPECT_CALL(
+        mock, sd_bus_message_append_basic(
+                  method, SD_BUS_TYPE_UINT64,
+                  MatcherCast<const void*>(
+                      SafeMatcherCast<const uint64_t*>(Pointee(Eq(address))))))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock,
+                sd_bus_message_append_basic(
+                    method, SD_BUS_TYPE_UINT64,
+                    MatcherCast<const void*>(SafeMatcherCast<const uint64_t*>(
+                        Pointee(Eq<uint64_t>(num_bytes))))))
+        .WillOnce(Return(1));
+
+    EXPECT_CALL(mock, sd_bus_call(_,          // sd_bus *bus,
+                                  method,     // sd_bus_message *m
+                                  _,          // uint64_t timeout
+                                  NotNull(),  // sd_bus_error *ret_error
+                                  NotNull())) // sd_bus_message **reply
+        .WillOnce(DoAll(SetArgPointee<3>(SD_BUS_ERROR_NULL),
+                        SetArgPointee<4>(msg), // reply
+                        Return(sd_bus_call_return_value)));
+
+    if (sd_bus_call_return_value >= 0)
+    {
+        EXPECT_CALL(mock,
+                    sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY,
+                                                   StrEq(SD_BUS_TYPE_BYTE_STR)))
+            .WillOnce(Return(1));
+
+        if (num_bytes_returned == NUM_BYTES_RETURNED_EQ_NUM_BYTES)
+        {
+            num_bytes_returned = num_bytes;
+        }
+        for (auto i = num_bytes_returned - 1; i >= 0; --i)
+        {
+            EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0))
+                .WillOnce(Return(0));
+
+            const uint8_t byte = (data >> (8 * i)) & 0xff;
+            EXPECT_CALL(mock, sd_bus_message_read_basic(msg, SD_BUS_TYPE_BYTE,
+                                                        NotNull()))
+                .WillOnce(DoAll(AssignReadVal<uint8_t>(byte), Return(1)));
+        }
+
+        EXPECT_CALL(mock, sd_bus_message_at_end(msg, 0)).WillOnce(Return(1));
+
+        EXPECT_CALL(mock, sd_bus_message_exit_container(msg))
+            .WillOnce(Return(1));
+    }
+}
+
+TEST(HandlerTest, accelOobRead_Success)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = 1;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+
+    ExpectRead(mock, address, num_bytes, data, sd_bus_call_return_value);
+    EXPECT_EQ(data, h.accelOobRead("test/path", address, num_bytes));
+}
+
+TEST(HandlerTest, accelOobRead_Fail)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = -ENOTCONN;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+
+    ExpectRead(mock, address, num_bytes, data, sd_bus_call_return_value);
+    EXPECT_THROW(h.accelOobRead("test/path", address, num_bytes),
+                 IpmiException);
+}
+
+TEST(HandlerTest, accelOobRead_TooFewBytesReturned)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = 1;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+    constexpr uint8_t num_bytes_returned = num_bytes - 1;
+
+    ExpectRead(mock, address, num_bytes, data, sd_bus_call_return_value,
+               num_bytes_returned);
+    EXPECT_THROW(h.accelOobRead("test/path", address, num_bytes),
+                 IpmiException);
+}
+
+TEST(HandlerTest, accelOobRead_TooManyBytesReturned)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = 1;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+    constexpr uint8_t num_bytes_returned = sizeof(uint64_t) + 1;
+
+    ExpectRead(mock, address, num_bytes, data, sd_bus_call_return_value,
+               num_bytes_returned);
+    EXPECT_THROW(h.accelOobRead("test/path", address, num_bytes),
+                 IpmiException);
+}
+
+void ExpectWrite(StrictMock<sdbusplus::SdBusMock>& mock, uint64_t address,
+                 uint8_t num_bytes, uint64_t data, int sd_bus_call_return_value)
+{
+    ::testing::InSequence s;
+
+    // These must be nullptr or sd_bus_message_unref will seg fault.
+    constexpr sd_bus_message* method = nullptr;
+
+    EXPECT_CALL(mock, sd_bus_message_new_method_call(
+                          _,         // sd_bus *bus,
+                          NotNull(), // sd_bus_message **m
+                          StrEq("com.google.custom_accel"),
+                          StrEq("/com/google/customAccel/test/path"),
+                          StrEq("com.google.custom_accel.BAR"), StrEq("Write")))
+        .WillOnce(DoAll(TraceDbus("sd_bus_message_new_method_call"),
+                        SetArgPointee<1>(method), Return(0)));
+
+    EXPECT_CALL(
+        mock, sd_bus_message_append_basic(
+                  method, SD_BUS_TYPE_UINT64,
+                  MatcherCast<const void*>(
+                      SafeMatcherCast<const uint64_t*>(Pointee(Eq(address))))))
+        .WillOnce(DoAll(TraceDbus("sd_bus_message_append_basic(address) -> 1"),
+                        Return(1)));
+
+    EXPECT_CALL(mock,
+                sd_bus_message_open_container(method, SD_BUS_TYPE_ARRAY,
+                                              StrEq(SD_BUS_TYPE_BYTE_STR)))
+        .WillOnce(DoAll(TraceDbus("sd_bus_message_open_container(a, y) -> 0"),
+                        Return(0)));
+
+    for (auto i = 0; i < num_bytes; ++i)
+    {
+        const uint8_t byte = (data >> (8 * i)) & 0xff;
+
+        EXPECT_CALL(
+            mock, sd_bus_message_append_basic(
+                      method, SD_BUS_TYPE_BYTE,
+                      MatcherCast<const void*>(
+                          SafeMatcherCast<const uint8_t*>(Pointee(Eq(byte))))))
+            .WillOnce(
+                DoAll(TraceDbus2("sd_bus_message_append_basic"), Return(1)));
+    }
+
+    EXPECT_CALL(mock, sd_bus_message_close_container(method))
+        .WillOnce(DoAll(TraceDbus("sd_bus_message_close_container() -> 0"),
+                        Return(0)));
+
+    EXPECT_CALL(mock, sd_bus_call(_,         // sd_bus *bus,
+                                  method,    // sd_bus_message *m
+                                  _,         // uint64_t timeout
+                                  NotNull(), // sd_bus_error *ret_error
+                                  IsNull())) // sd_bus_message **reply
+        .WillOnce(DoAll(TraceDbus("sd_bus_call() -> ret_val"),
+                        SetArgPointee<3>(SD_BUS_ERROR_NULL),
+                        Return(sd_bus_call_return_value)));
+}
+
+TEST(HandlerTest, accelOobWrite_Success)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = 1;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+
+    ExpectWrite(mock, address, num_bytes, data, sd_bus_call_return_value);
+    EXPECT_NO_THROW(h.accelOobWrite("test/path", address, num_bytes, data));
+}
+
+TEST(HandlerTest, accelOobRead_TooManyBytesRequested)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t) + 1;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+
+    EXPECT_THROW(h.accelOobWrite("test/path", address, num_bytes, data),
+                 IpmiException);
+}
+
+TEST(HandlerTest, accelOobWrite_Fail)
+{
+    StrictMock<sdbusplus::SdBusMock> mock;
+    MockDbusHandler h(mock);
+
+    constexpr uint64_t address = 0x123456789abcdef;
+    constexpr uint8_t num_bytes = sizeof(uint64_t);
+    constexpr int sd_bus_call_return_value = -ENOTCONN;
+    constexpr uint64_t data = 0x13579bdf02468ace;
+
+    ExpectWrite(mock, address, num_bytes, data, sd_bus_call_return_value);
+    EXPECT_THROW(h.accelOobWrite("test/path", address, num_bytes, data),
+                 IpmiException);
+}
+
 // TODO: Add checks for other functions of handler.
 
 } // namespace ipmi
diff --git a/test/meson.build b/test/meson.build
index a9da8b9..9c579bd 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -21,6 +21,7 @@
   'entity',
   'eth',
   'flash',
+  'google_accel_oob',
   'handler',
   'machine',
   'pcie',