diff --git a/CMakeLists.txt b/CMakeLists.txt index f949dd0..9903c48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -575,6 +575,7 @@ if(LIBICSNEO_BUILD_UNIT_TESTS) test/unit/apperrordecodertest.cpp test/unit/icsneoc2.cpp test/unit/ethphyregistertest.cpp + test/unit/tc10test.cpp test/unit/windowsstrings.cpp test/unit/periodictest.cpp ) diff --git a/api/icsneocpp/event.cpp b/api/icsneocpp/event.cpp index 9ee0c52..f5ea1a8 100644 --- a/api/icsneocpp/event.cpp +++ b/api/icsneocpp/event.cpp @@ -117,6 +117,7 @@ static constexpr const char* GPTP_NOT_SUPPORTED = "GPTP clock synchronization is static constexpr const char* SETTING_NOT_AVAILABLE = "Requested a setting that is not available on this device"; static constexpr const char* DISK_FORMAT_NOT_SUPPORTED = "Disk formatting is not supported on this device."; static constexpr const char* DISK_FORMAT_INVALID_COUNT = "Disk format config disk count is mismatched with device disk count."; +static constexpr const char* TC10_REQUEST_FAILED = "The device rejected the TC10 request."; // Transport Errors static constexpr const char* FAILED_TO_READ = "A read operation failed."; @@ -381,6 +382,8 @@ const char* APIEvent::DescriptionForType(Type type) { return DISK_FORMAT_NOT_SUPPORTED; case Type::DiskFormatInvalidCount: return DISK_FORMAT_INVALID_COUNT; + case Type::TC10RequestFailed: + return TC10_REQUEST_FAILED; // VSA case Type::VSABufferCorrupted: diff --git a/bindings/python/icsneopy/api/event.cpp b/bindings/python/icsneopy/api/event.cpp index 165b732..9cb17d2 100644 --- a/bindings/python/icsneopy/api/event.cpp +++ b/bindings/python/icsneopy/api/event.cpp @@ -92,6 +92,7 @@ void init_event(pybind11::module_& m) { .value("SettingNotAvaiableDevice", APIEvent::Type::SettingNotAvaiableDevice) .value("DiskFormatNotSupported", APIEvent::Type::DiskFormatNotSupported) .value("DiskFormatInvalidCount", APIEvent::Type::DiskFormatInvalidCount) + .value("TC10RequestFailed", APIEvent::Type::TC10RequestFailed) .value("FailedToRead", APIEvent::Type::FailedToRead) .value("FailedToWrite", APIEvent::Type::FailedToWrite) .value("DriverFailedToOpen", APIEvent::Type::DriverFailedToOpen) diff --git a/device/device.cpp b/device/device.cpp index 4818e75..217da0f 100644 --- a/device/device.cpp +++ b/device/device.cpp @@ -3659,7 +3659,7 @@ bool Device::requestTC10Wake(Network::NetID network) { *(Network::NetID*)args.data() = network; auto msg = com->waitForMessageSync([&] { return com->sendCommand(ExtendedCommand::RequestTC10Wake, args); - }, std::make_shared(Message::Type::ExtendedResponse), std::chrono::milliseconds(1000)); + }, std::make_shared(ExtendedCommand::RequestTC10Wake), std::chrono::milliseconds(1000)); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); @@ -3672,7 +3672,11 @@ bool Device::requestTC10Wake(Network::NetID network) { return false; } - return resp->response == ExtendedResponse::OK; + if(resp->response != ExtendedResponse::OK) { + report(APIEvent::Type::TC10RequestFailed, APIEvent::Severity::Error); + return false; + } + return true; } bool Device::requestTC10Sleep(Network::NetID network) { @@ -3684,7 +3688,7 @@ bool Device::requestTC10Sleep(Network::NetID network) { *(Network::NetID*)args.data() = network; auto msg = com->waitForMessageSync([&] { return com->sendCommand(ExtendedCommand::RequestTC10Sleep, args); - }, std::make_shared(Message::Type::ExtendedResponse), std::chrono::milliseconds(1000)); + }, std::make_shared(ExtendedCommand::RequestTC10Sleep), std::chrono::milliseconds(1000)); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); @@ -3697,7 +3701,11 @@ bool Device::requestTC10Sleep(Network::NetID network) { return false; } - return typed->response == ExtendedResponse::OK; + if(typed->response != ExtendedResponse::OK) { + report(APIEvent::Type::TC10RequestFailed, APIEvent::Severity::Error); + return false; + } + return true; } bool Device::reboot(bool safe) { diff --git a/include/icsneo/api/event.h b/include/icsneo/api/event.h index 6cb688b..000b484 100644 --- a/include/icsneo/api/event.h +++ b/include/icsneo/api/event.h @@ -126,6 +126,7 @@ public: SettingNotAvaiableDevice = 0x2057, DiskFormatNotSupported = 0x2058, DiskFormatInvalidCount = 0x2059, + TC10RequestFailed = 0x205A, // Transport Events FailedToRead = 0x3000, diff --git a/test/unit/tc10test.cpp b/test/unit/tc10test.cpp new file mode 100644 index 0000000..6b71dd2 --- /dev/null +++ b/test/unit/tc10test.cpp @@ -0,0 +1,221 @@ +#include "gtest/gtest.h" + +#include +#include + +#include "icsneo/icsneocpp.h" +#include "icsneo/communication/driver.h" +#include "icsneo/communication/message/extendedresponsemessage.h" +#include "icsneo/communication/message/filter/extendedresponsefilter.h" +#include "icsneo/communication/message/tc10statusmessage.h" +#include "icsneo/device/tree/radmoon2/radmoon2.h" +#include "icsneo/device/tree/radmoon2/radmoon2zl.h" + +using namespace icsneo; + +namespace { + +class TC10TestDriver : public Driver { +public: + explicit TC10TestDriver(const device_eventhandler_t& handler) : Driver(handler) {} + + bool open() override { opened = true; return true; } + bool isOpen() override { return opened; } + bool close() override { opened = false; return true; } + driver_finder_t getFinder() override { return [](std::vector&) {}; } + + int writes = 0; + // Replies pushed, in order, when the host writes a command. + std::vector> replies; + +protected: + bool writeInternal(const std::vector&) override { + ++writes; + for(const auto& reply : replies) + pushRx(reply.data(), reply.size()); + replies.clear(); + return true; + } + +private: + bool opened = false; +}; + +std::vector longPacket(Network::NetID netid, const void* payload, size_t payloadSize) { + const uint16_t packetLength = static_cast(6 + payloadSize); + std::vector bytes(packetLength); + bytes[0] = 0xAA; + bytes[1] = 0x00; + bytes[2] = static_cast(packetLength & 0xff); + bytes[3] = static_cast((packetLength >> 8) & 0xff); + const auto id = static_cast(netid); + bytes[4] = static_cast(id & 0xff); + bytes[5] = static_cast((id >> 8) & 0xff); + std::memcpy(bytes.data() + 6, payload, payloadSize); + return bytes; +} + +std::vector genericReturn(ExtendedCommand command, ExtendedResponse code) { + ExtendedResponseMessage::PackedGenericResponse packed{}; + packed.header.command = ExtendedCommand::GenericReturn; + packed.header.length = static_cast(sizeof(packed) - sizeof(packed.header)); + packed.command = command; + packed.returnCode = code; + return longPacket(Network::NetID::ExtendedCommand, &packed, sizeof(packed)); +} + +// Extended body of a GetTC10Status reply. `tail` is extra payload past the two status bytes. +std::vector statusBody(uint8_t wake, uint8_t sleep, std::initializer_list tail = {}) { + const auto payloadLength = static_cast(2 + tail.size()); + std::vector body = { + 0x3f, 0x00, + static_cast(payloadLength & 0xff), + static_cast((payloadLength >> 8) & 0xff), + wake, + sleep, + }; + body.insert(body.end(), tail); + return body; +} + +class OpenTC10Device { +public: + TC10TestDriver* driver = nullptr; + std::shared_ptr device; + + OpenTC10Device() { + FoundDevice found; + std::memcpy(found.serial, "RN0001", 6); + found.makeDriver = [this](device_eventhandler_t handler, neodevice_t&) { + auto result = std::make_unique(handler); + driver = result.get(); + return result; + }; + device = std::make_shared(found); + device->com->readTaskWakeTimeout = std::chrono::milliseconds(20); + EXPECT_TRUE(device->com->open()); + } +}; + +FoundDevice moon2Found() { + FoundDevice found; + std::memcpy(found.serial, "RM0001", 6); + found.makeDriver = [](device_eventhandler_t handler, neodevice_t&) { + return std::make_unique(handler); + }; + return found; +} + +} + +TEST(TC10, WakeFilterMatchesOnlyThatCommand) { + ExtendedResponseFilter filter(ExtendedCommand::RequestTC10Wake); + EXPECT_TRUE(filter.match(std::make_shared( + ExtendedCommand::RequestTC10Wake, ExtendedResponse::OperationFailed))); + EXPECT_FALSE(filter.match(std::make_shared( + ExtendedCommand::RequestTC10Sleep, ExtendedResponse::OK))); + EXPECT_FALSE(filter.match(std::make_shared( + ExtendedCommand::GetDiskDetails, ExtendedResponse::OK))); +} + +TEST(TC10, SleepFilterMatchesOnlyThatCommand) { + ExtendedResponseFilter filter(ExtendedCommand::RequestTC10Sleep); + EXPECT_TRUE(filter.match(std::make_shared( + ExtendedCommand::RequestTC10Sleep, ExtendedResponse::OK))); + EXPECT_FALSE(filter.match(std::make_shared( + ExtendedCommand::RequestTC10Wake, ExtendedResponse::OK))); +} + +TEST(TC10, UnsupportedDeviceReportsNotSupported) { + auto device = std::make_shared(moon2Found()); + EXPECT_FALSE(device->requestTC10Wake(Network::NetID::AE_01)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NotSupported); + EXPECT_FALSE(device->requestTC10Sleep(Network::NetID::AE_01)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NotSupported); + EXPECT_FALSE(device->getTC10Status(Network::NetID::AE_01).has_value()); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NotSupported); +} + +TEST(TC10, WakeAcceptsItsOwnOk) { + OpenTC10Device tc10; + (void)GetLastError(); + tc10.driver->replies = { + genericReturn(ExtendedCommand::RequestTC10Sleep, ExtendedResponse::OperationFailed), + genericReturn(ExtendedCommand::RequestTC10Wake, ExtendedResponse::OK), + }; + EXPECT_TRUE(tc10.device->requestTC10Wake(Network::NetID::AE_02)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NoErrorFound); + EXPECT_EQ(tc10.driver->writes, 1); +} + +TEST(TC10, SleepAcceptsItsOwnOk) { + OpenTC10Device tc10; + (void)GetLastError(); + tc10.driver->replies = { + genericReturn(ExtendedCommand::RequestTC10Wake, ExtendedResponse::OperationFailed), + genericReturn(ExtendedCommand::RequestTC10Sleep, ExtendedResponse::OK), + }; + EXPECT_TRUE(tc10.device->requestTC10Sleep(Network::NetID::AE_01)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NoErrorFound); + EXPECT_EQ(tc10.driver->writes, 1); +} + +TEST(TC10, WakeReportsDeviceRejection) { + OpenTC10Device tc10; + tc10.driver->replies = { + genericReturn(ExtendedCommand::RequestTC10Sleep, ExtendedResponse::OK), + genericReturn(ExtendedCommand::RequestTC10Wake, ExtendedResponse::OperationFailed), + }; + EXPECT_FALSE(tc10.device->requestTC10Wake(Network::NetID::AE_02)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::TC10RequestFailed); + EXPECT_EQ(tc10.driver->writes, 1); +} + +TEST(TC10, SleepReportsDeviceRejection) { + OpenTC10Device tc10; + tc10.driver->replies = { + genericReturn(ExtendedCommand::RequestTC10Wake, ExtendedResponse::OK), + genericReturn(ExtendedCommand::RequestTC10Sleep, ExtendedResponse::InvalidParameter), + }; + EXPECT_FALSE(tc10.device->requestTC10Sleep(Network::NetID::AE_01)); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::TC10RequestFailed); + EXPECT_EQ(tc10.driver->writes, 1); +} + +TEST(TC10Status, DecodesTheStatusPair) { + auto msg = TC10StatusMessage::DecodeToMessage(statusBody(1, 1)); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->wakeStatus, TC10WakeStatus::WakeReceived); + EXPECT_EQ(msg->sleepStatus, TC10SleepStatus::SleepReceived); +} + +TEST(TC10Status, RejectsTruncatedOrForeignPayloads) { + EXPECT_EQ(TC10StatusMessage::DecodeToMessage({}), nullptr); + auto bytes = statusBody(0, 0); + EXPECT_EQ(TC10StatusMessage::DecodeToMessage({bytes.begin(), bytes.begin() + 5}), nullptr); + std::vector shortLength = {0x3f, 0x00, 0x01, 0x00, 0x00, 0x00}; + EXPECT_EQ(TC10StatusMessage::DecodeToMessage(shortLength), nullptr); + std::vector foreign = {0x3d, 0x00, 0x02, 0x00, 0x00, 0x00}; + EXPECT_EQ(TC10StatusMessage::DecodeToMessage(foreign), nullptr); +} + +TEST(TC10Status, DropsBytesAfterTheStatusPair) { + // Tail is little-endian AE_02 (37). The decoded message has no network, so this still succeeds. + auto msg = TC10StatusMessage::DecodeToMessage(statusBody(0, 1, {37, 0})); + ASSERT_NE(msg, nullptr); + EXPECT_EQ(msg->wakeStatus, TC10WakeStatus::NoWakeReceived); + EXPECT_EQ(msg->sleepStatus, TC10SleepStatus::SleepReceived); +} + +TEST(TC10, StatusReturnsTheDecodedPair) { + OpenTC10Device tc10; + (void)GetLastError(); + const auto body = statusBody(1, 0); + tc10.driver->replies = {longPacket(Network::NetID::ExtendedCommand, body.data(), body.size())}; + const auto status = tc10.device->getTC10Status(Network::NetID::AE_01); + ASSERT_TRUE(status.has_value()); + EXPECT_EQ(status->wakeStatus, TC10WakeStatus::WakeReceived); + EXPECT_EQ(status->sleepStatus, TC10SleepStatus::NoSleepReceived); + EXPECT_EQ(GetLastError().getType(), APIEvent::Type::NoErrorFound); + EXPECT_EQ(tc10.driver->writes, 1); +}