diff --git a/common/rdm/DummyResponder.cpp b/common/rdm/DummyResponder.cpp index 8449b1226..20241e7d3 100644 --- a/common/rdm/DummyResponder.cpp +++ b/common/rdm/DummyResponder.cpp @@ -172,6 +172,15 @@ const ResponderOps<DummyResponder>::ParamHandler { PID_TEST_DATA, &DummyResponder::GetTestData, &DummyResponder::SetTestData}, + { PID_METADATA_PARAMETER_VERSION, + &DummyResponder::GetMetadataParameterVersion, + NULL}, + { PID_METADATA_JSON, + &DummyResponder::GetMetadataJSON, + NULL}, + { PID_METADATA_JSON_URL, + &DummyResponder::GetMetadataJSONURL, + NULL}, { OLA_MANUFACTURER_PID_CODE_VERSION, &DummyResponder::GetOlaCodeVersion, NULL}, @@ -460,7 +469,7 @@ RDMResponse *DummyResponder::GetProductURL( request, "https://openlighting.org/rdm-tools/dummy-responders/", 0, - UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec + UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec } RDMResponse *DummyResponder::GetFirmwareURL( @@ -469,7 +478,7 @@ RDMResponse *DummyResponder::GetFirmwareURL( request, "https://github.com/OpenLightingProject/ola", 0, - UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec + UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec } RDMResponse *DummyResponder::GetTestData(const RDMRequest *request) { @@ -480,6 +489,58 @@ RDMResponse *DummyResponder::SetTestData(const RDMRequest *request) { return ResponderHelper::SetTestData(request); } +RDMResponse *DummyResponder::GetMetadataParameterVersion( + const RDMRequest *request) { + // Check that it's OLA_MANUFACTURER_PID_CODE_VERSION being requested + uint16_t parameter_id; + if (!ResponderHelper::ExtractUInt16(request, ¶meter_id)) { + return NackWithReason(request, NR_FORMAT_ERROR); + } + + if (parameter_id != OLA_MANUFACTURER_PID_CODE_VERSION) { + OLA_WARN << "Dummy responder received metadata parameter version request " + << "with unknown PID, expected " + << OLA_MANUFACTURER_PID_CODE_VERSION << ", got " << parameter_id; + return NackWithReason(request, NR_DATA_OUT_OF_RANGE); + } else { + return ResponderHelper::GetMetadataParameterVersion( + request, + OLA_MANUFACTURER_PID_CODE_VERSION, + OLA_MANUFACTURER_PID_JSON_VERSION_CODE_VERSION); + } +} + +RDMResponse *DummyResponder::GetMetadataJSON( + const RDMRequest *request) { + // Check that it's OLA_MANUFACTURER_PID_CODE_VERSION being requested + uint16_t parameter_id; + if (!ResponderHelper::ExtractUInt16(request, ¶meter_id)) { + return NackWithReason(request, NR_FORMAT_ERROR); + } + + if (parameter_id != OLA_MANUFACTURER_PID_CODE_VERSION) { + OLA_WARN << "Dummy responder received metadata JSON request with unknown " + << "PID, expected " + << OLA_MANUFACTURER_PID_CODE_VERSION << ", got " << parameter_id; + return NackWithReason(request, NR_DATA_OUT_OF_RANGE); + } else { + return ResponderHelper::GetMetadataJSON( + request, + OLA_MANUFACTURER_PID_CODE_VERSION, + OLA_MANUFACTURER_PID_JSON_CODE_VERSION); + } +} + +RDMResponse *DummyResponder::GetMetadataJSONURL( + const RDMRequest *request) { + return ResponderHelper::GetString( + request, + // TODO(Peter): Consider what this should actually be permanently + "https://docs.openlighting.org/ola/json/latest/metadata/0x0001.json", + 0, + UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec +} + RDMResponse *DummyResponder::GetOlaCodeVersion( const RDMRequest *request) { return ResponderHelper::GetString(request, VERSION); diff --git a/common/rdm/OpenLightingEnums.cpp b/common/rdm/OpenLightingEnums.cpp index a2fca2377..08b118780 100644 --- a/common/rdm/OpenLightingEnums.cpp +++ b/common/rdm/OpenLightingEnums.cpp @@ -24,5 +24,11 @@ namespace rdm { const char OLA_MANUFACTURER_LABEL[] = "Open Lighting Project"; const char OLA_MANUFACTURER_URL[] = "https://openlighting.org/"; + +const char OLA_MANUFACTURER_PID_JSON_CODE_VERSION[] = "{\"name\":" + "\"CODE_VERSION\",\"manufacturer_id\":31344,\"pid\":32769,\"version\":1," + "\"get_request_subdevice_range\":[\"root\",\"subdevices\"]," + "\"get_request\":[],\"get_response\":[{\"name\":\"code_version\"," + "\"type\":\"string\",""\"maxLength\":32,\"restrictToASCII\":true}]}"; } // namespace rdm } // namespace ola diff --git a/common/rdm/ResponderHelper.cpp b/common/rdm/ResponderHelper.cpp index b4528f3c2..09b461f9a 100644 --- a/common/rdm/ResponderHelper.cpp +++ b/common/rdm/ResponderHelper.cpp @@ -965,7 +965,7 @@ RDMResponse *ResponderHelper::GetParamDescription( uint32_t min_value, uint32_t default_value, uint32_t max_value, - string description, + const string description, uint8_t queued_message_count) { PACK( struct parameter_description_s { @@ -1019,7 +1019,7 @@ RDMResponse *ResponderHelper::GetASCIIParamDescription( const RDMRequest *request, uint16_t pid, rdm_command_class command_class, - string description, + const string description, uint8_t queued_message_count) { return GetParamDescription( request, @@ -1041,7 +1041,7 @@ RDMResponse *ResponderHelper::GetBitFieldParamDescription( uint16_t pid, uint8_t pdl_size, rdm_command_class command_class, - string description, + const string description, uint8_t queued_message_count) { return GetParamDescription( request, @@ -1110,6 +1110,65 @@ RDMResponse *ResponderHelper::SetTestData( queued_message_count); } +/** + * Get NSC comms status + */ +RDMResponse *ResponderHelper::GetCommsStatusNSC( + const RDMRequest *request, + const NSCStatus *status, + uint8_t queued_message_count) { + if (request->ParamDataSize()) { + return NackWithReason(request, NR_FORMAT_ERROR, queued_message_count); + } + + PACK( + struct comms_status_nsc_s { + uint8_t supported_fields; + uint32_t additive_checksum; + uint32_t packet_count; + uint16_t most_recent_slot_count; + uint16_t min_slot_count; + uint16_t max_slot_count; + uint32_t packet_error_count; + }); + STATIC_ASSERT(sizeof(comms_status_nsc_s) == 19); + + struct comms_status_nsc_s comms_status_nsc; + comms_status_nsc.supported_fields = status->SupportedFieldsBitMask(); + comms_status_nsc.additive_checksum = HostToNetwork( + status->AdditiveChecksum()); + comms_status_nsc.packet_count = HostToNetwork(status->PacketCount()); + comms_status_nsc.most_recent_slot_count = HostToNetwork( + status->MostRecentSlotCount()); + comms_status_nsc.min_slot_count = HostToNetwork(status->MinSlotCount()); + comms_status_nsc.max_slot_count = HostToNetwork(status->MaxSlotCount()); + comms_status_nsc.packet_error_count = HostToNetwork( + status->PacketErrorCount()); + return GetResponseFromData( + request, + reinterpret_cast<const uint8_t*>(&comms_status_nsc), + sizeof(comms_status_nsc), + RDM_ACK, + queued_message_count); +} + +/** + * Set NSC comms status + */ +RDMResponse *ResponderHelper::SetCommsStatusNSC( + const RDMRequest *request, + NSCStatus *status, + uint8_t queued_message_count) { + if (request->ParamDataSize()) { + return NackWithReason(request, NR_FORMAT_ERROR, queued_message_count); + } + + // Reset the counts... + status->Reset(); + + return GetResponseFromData(request, NULL, queued_message_count); +} + RDMResponse *ResponderHelper::GetListTags( const RDMRequest *request, const TagSet *tag_set, @@ -1187,6 +1246,63 @@ RDMResponse *ResponderHelper::SetClearTags( return ResponderHelper::EmptySetResponse(request, queued_message_count); } +RDMResponse *ResponderHelper::GetMetadataParameterVersion( + const RDMRequest *request, + uint16_t pid, + uint16_t version, + uint8_t queued_message_count) { + PACK( + struct metadata_parameter_version_s { + uint16_t pid; + uint16_t version; + }); + STATIC_ASSERT(sizeof(metadata_parameter_version_s) == 4); + + struct metadata_parameter_version_s metadata_param_version; + metadata_param_version.pid = HostToNetwork(pid); + metadata_param_version.version = HostToNetwork(version); + + return GetResponseFromData( + request, + reinterpret_cast<uint8_t*>(&metadata_param_version), + sizeof(metadata_parameter_version_s), + RDM_ACK, + queued_message_count); +} + +RDMResponse *ResponderHelper::GetMetadataJSON( + const RDMRequest *request, + uint16_t pid, + const string json, + uint8_t queued_message_count) { + PACK( + struct metadata_json_s { + uint16_t pid; + // TODO(Peter): This should effectively be unlimited...? + char json[(UINT8_MAX - 2)]; + }); + STATIC_ASSERT(sizeof(metadata_json_s) == UINT8_MAX); + + struct metadata_json_s metadata_json; + metadata_json.pid = HostToNetwork(pid); + + size_t str_len = min(json.size(), + sizeof(metadata_json.json)); + strncpy(metadata_json.json, json.c_str(), str_len); + + unsigned int param_data_size = ( + sizeof(metadata_json) - + sizeof(metadata_json.json) + str_len); + + return GetResponseFromData( + request, + reinterpret_cast<uint8_t*>(&metadata_json), + param_data_size, + RDM_ACK, + queued_message_count); +} + + /** * @brief Handle a request that returns a string * @note this truncates the string to max_length diff --git a/common/utils/DmxBuffer.cpp b/common/utils/DmxBuffer.cpp index b0a3b65fb..213c6763c 100644 --- a/common/utils/DmxBuffer.cpp +++ b/common/utils/DmxBuffer.cpp @@ -110,6 +110,15 @@ bool DmxBuffer::operator!=(const DmxBuffer &other) const { } +unsigned int DmxBuffer::AdditiveChecksum() const { + unsigned int checksum = 0; + for (unsigned int i = 0; i < Size(); i++) { + checksum += m_data[i]; + } + return checksum; +} + + bool DmxBuffer::HTPMerge(const DmxBuffer &other) { if (!m_data) { if (!Init()) diff --git a/common/utils/DmxBufferTest.cpp b/common/utils/DmxBufferTest.cpp index 74e6fd2e2..0b9847133 100644 --- a/common/utils/DmxBufferTest.cpp +++ b/common/utils/DmxBufferTest.cpp @@ -37,6 +37,7 @@ class DmxBufferTest: public CppUnit::TestFixture { CPPUNIT_TEST(testStringGetSet); CPPUNIT_TEST(testAssign); CPPUNIT_TEST(testCopy); + CPPUNIT_TEST(testAdditiveChecksum); CPPUNIT_TEST(testMerge); CPPUNIT_TEST(testStringToDmx); CPPUNIT_TEST(testCopyOnWrite); @@ -52,6 +53,7 @@ class DmxBufferTest: public CppUnit::TestFixture { void testAssign(); void testStringGetSet(); void testCopy(); + void testAdditiveChecksum(); void testMerge(); void testStringToDmx(); void testCopyOnWrite(); @@ -261,6 +263,21 @@ void DmxBufferTest::testCopy() { } +/* + * Check that the additive checksum works + */ +void DmxBufferTest::testAdditiveChecksum() { + DmxBuffer buffer1(TEST_DATA, sizeof(TEST_DATA)); + OLA_ASSERT_EQ(15u, buffer1.AdditiveChecksum()); + + DmxBuffer buffer2(TEST_DATA2, sizeof(TEST_DATA2)); + OLA_ASSERT_EQ(45u, buffer2.AdditiveChecksum()); + + DmxBuffer buffer3(TEST_DATA3, sizeof(TEST_DATA3)); + OLA_ASSERT_EQ(33u, buffer3.AdditiveChecksum()); +} + + /* * Check that HTP Merging works */ diff --git a/include/ola/DmxBuffer.h b/include/ola/DmxBuffer.h index 3dfd73adf..50c94943c 100644 --- a/include/ola/DmxBuffer.h +++ b/include/ola/DmxBuffer.h @@ -110,6 +110,12 @@ class DmxBuffer { */ unsigned int Size() const { return m_length; } + /** + * @brief Additive checksum of DmxBuffer + * @return the additive checksum of slots in the buffer. + */ + unsigned int AdditiveChecksum() const; + /** * @brief HTP Merge from another DmxBuffer. * @param other the DmxBuffer to HTP merge into this one diff --git a/include/ola/rdm/DummyResponder.h b/include/ola/rdm/DummyResponder.h index f08e70272..ce1750420 100644 --- a/include/ola/rdm/DummyResponder.h +++ b/include/ola/rdm/DummyResponder.h @@ -135,6 +135,9 @@ class DummyResponder: public RDMControllerInterface { RDMResponse *GetFirmwareURL(const RDMRequest *request); RDMResponse *GetTestData(const RDMRequest *request); RDMResponse *SetTestData(const RDMRequest *request); + RDMResponse *GetMetadataParameterVersion(const RDMRequest *request); + RDMResponse *GetMetadataJSON(const RDMRequest *request); + RDMResponse *GetMetadataJSONURL(const RDMRequest *request); static const ResponderOps<DummyResponder>::ParamHandler PARAM_HANDLERS[]; static const uint8_t DEFAULT_PERSONALITY = 2; diff --git a/include/ola/rdm/Makefile.mk b/include/ola/rdm/Makefile.mk index d7ba60563..11c9a4704 100644 --- a/include/ola/rdm/Makefile.mk +++ b/include/ola/rdm/Makefile.mk @@ -34,6 +34,7 @@ olardminclude_HEADERS = \ include/ola/rdm/RDMReply.h \ include/ola/rdm/ResponderHelper.h \ include/ola/rdm/ResponderLoadSensor.h \ + include/ola/rdm/ResponderNSCStatus.h \ include/ola/rdm/ResponderOps.h \ include/ola/rdm/ResponderOpsPrivate.h \ include/ola/rdm/ResponderPersonality.h \ diff --git a/include/ola/rdm/OpenLightingEnums.h b/include/ola/rdm/OpenLightingEnums.h index 42156dd87..9260ce920 100644 --- a/include/ola/rdm/OpenLightingEnums.h +++ b/include/ola/rdm/OpenLightingEnums.h @@ -50,6 +50,13 @@ typedef enum { OLA_MANUFACTURER_PID_CODE_VERSION = 0x8001, } rdm_ola_manufacturer_pid; +// TODO(Peter): Some sort of vector against rdm_ola_manufacturer_pid or +// something nicer? +typedef enum { + OLA_MANUFACTURER_PID_JSON_VERSION_SERIAL_NUMBER = 1, + OLA_MANUFACTURER_PID_JSON_VERSION_CODE_VERSION = 1, +} rdm_ola_manufacturer_pid_json_version; + /** * Also see the list here * https://wiki.openlighting.org/index.php/Open_Lighting_Allocations#RDM_Model_Numbers @@ -77,6 +84,7 @@ typedef enum { extern const char OLA_MANUFACTURER_LABEL[]; extern const char OLA_MANUFACTURER_URL[]; +extern const char OLA_MANUFACTURER_PID_JSON_CODE_VERSION[]; } // namespace rdm } // namespace ola #endif // INCLUDE_OLA_RDM_OPENLIGHTINGENUMS_H_ diff --git a/include/ola/rdm/RDMEnums.h b/include/ola/rdm/RDMEnums.h index 8f8b3a10b..46b9f6e4c 100644 --- a/include/ola/rdm/RDMEnums.h +++ b/include/ola/rdm/RDMEnums.h @@ -761,6 +761,29 @@ static const uint8_t DNS_NAME_SERVER_MAX_INDEX = 2; // Consts for E1.37-5 static const uint16_t MAX_RDM_TEST_DATA_PATTERN_LENGTH = 4096; +// bit masks for NSC status +static const uint8_t NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE = 0x01; +static const uint8_t NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE = 0x02; +static const uint8_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE = 0x04; +static const uint8_t NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE = 0x08; +static const uint8_t NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE = 0x10; +static const uint8_t NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE = 0x20; + +// Consts for NSC status when unsupported +static const uint32_t NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED = 0xFFFFFFFF; +static const uint32_t NSC_STATUS_PACKET_COUNT_UNSUPPORTED = 0xFFFFFFFF; +static const uint16_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED = 0xFFFF; +static const uint16_t NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED = 0xFFFF; +static const uint16_t NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED = 0xFFFF; +static const uint32_t NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED = 0xFFFFFFFF; + +// Consts for NSC status max range +static const uint32_t NSC_STATUS_PACKET_COUNT_MAX = 0xFFFFFFFE; +static const uint16_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_MAX = 0xFFFE; +static const uint16_t NSC_STATUS_MIN_SLOT_COUNT_MAX = 0xFFFE; +static const uint16_t NSC_STATUS_MAX_SLOT_COUNT_MAX = 0xFFFE; +static const uint32_t NSC_STATUS_PACKET_ERROR_COUNT_MAX = 0xFFFFFFFE; + // The shipping lock states typedef enum { SHIPPING_LOCK_STATE_UNLOCKED = 0x00, diff --git a/include/ola/rdm/ResponderHelper.h b/include/ola/rdm/ResponderHelper.h index 4256c4f62..1edcd22c1 100644 --- a/include/ola/rdm/ResponderHelper.h +++ b/include/ola/rdm/ResponderHelper.h @@ -32,6 +32,7 @@ #include <ola/network/Interface.h> #include <ola/rdm/NetworkManagerInterface.h> #include <ola/rdm/RDMCommand.h> +#include <ola/rdm/ResponderNSCStatus.h> #include <ola/rdm/ResponderPersonality.h> #include <ola/rdm/ResponderSensor.h> #include <ola/rdm/ResponderTagSet.h> @@ -151,20 +152,20 @@ class ResponderHelper { uint32_t min_value, uint32_t default_value, uint32_t max_value, - std::string description, + const std::string description, uint8_t queued_message_count = 0); static RDMResponse *GetASCIIParamDescription( const RDMRequest *request, uint16_t pid, rdm_command_class command_class, - std::string description, + const std::string description, uint8_t queued_message_count = 0); static RDMResponse *GetBitFieldParamDescription( const RDMRequest *request, uint16_t pid, uint8_t pdl_size, rdm_command_class command_class, - std::string description, + const std::string description, uint8_t queued_message_count = 0); static RDMResponse *GetRealTimeClock( @@ -226,6 +227,16 @@ class ResponderHelper { const RDMRequest *request, uint8_t queued_message_count = 0); + static RDMResponse *GetCommsStatusNSC( + const RDMRequest *request, + const NSCStatus *status, + uint8_t queued_message_count = 0); + + static RDMResponse *SetCommsStatusNSC( + const RDMRequest *request, + NSCStatus *status, + uint8_t queued_message_count = 0); + static RDMResponse *GetListTags( const RDMRequest *request, const TagSet *tag_set, @@ -251,6 +262,18 @@ class ResponderHelper { TagSet *tag_set, uint8_t queued_message_count = 0); + static RDMResponse *GetMetadataParameterVersion( + const RDMRequest *request, + uint16_t pid, + uint16_t version, + uint8_t queued_message_count = 0); + + static RDMResponse *GetMetadataJSON( + const RDMRequest *request, + uint16_t pid, + const std::string json, + uint8_t queued_message_count = 0); + // Generic Helpers. static RDMResponse *GetString( const RDMRequest *request, diff --git a/include/ola/rdm/ResponderNSCStatus.h b/include/ola/rdm/ResponderNSCStatus.h new file mode 100644 index 000000000..c5740eff4 --- /dev/null +++ b/include/ola/rdm/ResponderNSCStatus.h @@ -0,0 +1,239 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * ResponderNSCStatus.h + * Manages the NSC status for a RDM responder. + * Copyright (C) 2025 Peter Newman + */ + +/** + * @addtogroup rdm_resp + * @{ + * @file ResponderNSCStatus.h + * @brief Manages the information about NSC status. + * @} + */ + +#ifndef INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_ +#define INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_ + +#include <ola/rdm/RDMEnums.h> +#include <ola/DmxBuffer.h> +#include <stdint.h> + +#include <algorithm> +#include <string> +#include <vector> + +namespace ola { +namespace rdm { + +/** + * @brief Holds information about NSC status. + */ +class NSCStatus { + public: + struct NSCStatusOptions { + public: + bool additive_checksum_support; + bool packet_count_support; + bool most_recent_slot_count_support; + bool min_slot_count_support; + bool max_slot_count_support; + bool packet_error_count_support; + + // NSCStatusOptions constructor to set all options, for use in + // initialisation lists. This also sets the defaults if called with no + // args + NSCStatusOptions(bool _additive_checksum_support = true, + bool _packet_count_support = true, + bool _most_recent_slot_count_support = true, + bool _min_slot_count_support = true, + bool _max_slot_count_support = true, + bool _packet_error_count_support = false) + : additive_checksum_support(_additive_checksum_support), + packet_count_support(_packet_count_support), + most_recent_slot_count_support(_most_recent_slot_count_support), + min_slot_count_support(_min_slot_count_support), + max_slot_count_support(_max_slot_count_support), + packet_error_count_support(_packet_error_count_support) { + } + }; + + explicit NSCStatus(const NSCStatusOptions &options) + : m_additive_checksum_support(options.additive_checksum_support), + m_packet_count_support(options.packet_count_support), + m_most_recent_slot_count_support( + options.most_recent_slot_count_support), + m_min_slot_count_support(options.min_slot_count_support), + m_max_slot_count_support(options.max_slot_count_support), + m_packet_error_count_support(options.packet_error_count_support), + m_additive_checksum(0), + m_packet_count(0), + m_most_recent_slot_count(0), + m_min_slot_count(0), + m_max_slot_count(0), + m_packet_error_count(0) { + } + virtual ~NSCStatus() {} + + uint32_t AdditiveChecksum() const { + if (m_additive_checksum_support) { + return m_additive_checksum; + } else { + return NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED; + } + } + + uint32_t PacketCount() const { + if (m_packet_count_support) { + return m_packet_count; + } else { + return NSC_STATUS_PACKET_COUNT_UNSUPPORTED; + } + } + + uint16_t MostRecentSlotCount() const { + if (m_most_recent_slot_count_support) { + return m_most_recent_slot_count; + } else { + return NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED; + } + } + + uint16_t MinSlotCount() const { + if (m_min_slot_count_support) { + return m_min_slot_count; + } else { + return NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED; + } + } + + uint16_t MaxSlotCount() const { + if (m_max_slot_count_support) { + return m_max_slot_count; + } else { + return NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED; + } + } + + uint32_t PacketErrorCount() const { + if (m_packet_error_count_support) { + return m_packet_error_count; + } else { + return NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED; + } + } + + /** + * @brief Update the statistics we can from the DmxBuffer + * @note A DmxBuffer can only contain 512 slots (plus the start code) so this + * will limit some edge cases. + * @sa ReportError() + */ + void UpdateStats(const DmxBuffer &buffer) { + // Size() + 1 to account for the start code + + m_additive_checksum = buffer.AdditiveChecksum(); + m_most_recent_slot_count = std::min(NSC_STATUS_MOST_RECENT_SLOT_COUNT_MAX, + (uint16_t) (buffer.Size() + 1)); + + if (m_packet_count == 0) { + // We must set the buffer size explicitly here on the first packet or + // we'd be permanently stuck at 0 + m_min_slot_count = std::min(NSC_STATUS_MIN_SLOT_COUNT_MAX, + (uint16_t) (buffer.Size() + 1)); + } else { + m_min_slot_count = std::min(NSC_STATUS_MIN_SLOT_COUNT_MAX, + std::min(m_min_slot_count, + (uint16_t) (buffer.Size() + 1))); + } + + m_max_slot_count = std::min(NSC_STATUS_MAX_SLOT_COUNT_MAX, + std::max(m_max_slot_count, + (uint16_t) (buffer.Size() + 1))); + + // Update packet counter last so we can use it to track whether this is the + // first packet or not + if (m_packet_count < NSC_STATUS_PACKET_COUNT_MAX) { + m_packet_count++; + } + + // We can't establish error states from the buffer, so don't touch them + // here + } + + /** + * @brief Report a NSC error to increment that counter + */ + void ReportError() { + if (m_packet_error_count < NSC_STATUS_PACKET_ERROR_COUNT_MAX) { + m_packet_error_count++; + } + } + + /** + * @brief Reset the NSC stats. + */ + void Reset() { + m_additive_checksum = 0; + m_packet_count = 0; + m_most_recent_slot_count = 0; + m_min_slot_count = 0; + m_max_slot_count = 0; + m_packet_error_count = 0; + } + + uint8_t SupportedFieldsBitMask() const { + uint8_t bit_mask = 0; + + if (m_additive_checksum_support) { + bit_mask |= NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE; + } + if (m_packet_count_support) { + bit_mask |= NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE; + } + if (m_most_recent_slot_count_support) { + bit_mask |= NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE; + } + if (m_min_slot_count_support) { + bit_mask |= NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE; + } + if (m_max_slot_count_support) { + bit_mask |= NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE; + } + if (m_packet_error_count_support) { + bit_mask |= NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE; + } + return bit_mask; + } + + protected: + const bool m_additive_checksum_support; + const bool m_packet_count_support; + const bool m_most_recent_slot_count_support; + const bool m_min_slot_count_support; + const bool m_max_slot_count_support; + const bool m_packet_error_count_support; + uint32_t m_additive_checksum; + uint32_t m_packet_count; + uint16_t m_most_recent_slot_count; + uint16_t m_min_slot_count; + uint16_t m_max_slot_count; + uint32_t m_packet_error_count; +}; +} // namespace rdm +} // namespace ola +#endif // INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_ diff --git a/plugins/dummy/DummyPortTest.cpp b/plugins/dummy/DummyPortTest.cpp index 7593c46c7..3634a29be 100644 --- a/plugins/dummy/DummyPortTest.cpp +++ b/plugins/dummy/DummyPortTest.cpp @@ -242,6 +242,9 @@ void DummyPortTest::testSupportedParams() { uint16_t supported_params[] = { ola::rdm::PID_TEST_DATA, + ola::rdm::PID_METADATA_PARAMETER_VERSION, + ola::rdm::PID_METADATA_JSON, + ola::rdm::PID_METADATA_JSON_URL, ola::rdm::PID_PRODUCT_DETAIL_ID_LIST, ola::rdm::PID_DEVICE_MODEL_DESCRIPTION, ola::rdm::PID_MANUFACTURER_LABEL, diff --git a/python/ola/RDMConstants.py b/python/ola/RDMConstants.py index 736f75051..cb628bc84 100644 --- a/python/ola/RDMConstants.py +++ b/python/ola/RDMConstants.py @@ -20,8 +20,13 @@ __author__ = 'nomis52@gmail.com (Simon Newton)' +RDM_MAX_PARAM_DATA_LENGTH = 231 + RDM_ZERO_FOOTPRINT_DMX_ADDRESS = 0xFFFF +RDM_ESTA_PID_MIN = 0x0000 +RDM_ESTA_PID_MAX = 0x7FDF + RDM_MANUFACTURER_PID_MIN = 0x8000 RDM_MANUFACTURER_PID_MAX = 0xFFDF @@ -43,6 +48,20 @@ RDM_MAX_TEST_DATA_PATTERN_LENGTH = 4096 +RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE = 0x01 +RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE = 0x02 +RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE = 0x04 +RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE = 0x08 +RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE = 0x10 +RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE = 0x20 + +RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED = 0xFFFFFFFF +RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED = 0xFFFFFFFF +RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED = 0xFFFF +RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED = 0xFFFF +RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED = 0xFFFF +RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED = 0xFFFFFFFF + def _ReverseDict(input): output = {} diff --git a/tools/rdm/ModelCollector.py b/tools/rdm/ModelCollector.py index f688bfe26..88fe6b4ca 100644 --- a/tools/rdm/ModelCollector.py +++ b/tools/rdm/ModelCollector.py @@ -59,7 +59,10 @@ class ModelCollector(object): SLOT_DESCRIPTION, MANUFACTURER_URL, PRODUCT_URL, - FIRMWARE_URL) = range(18) + FIRMWARE_URL, + METADATA_PARAMETER_VERSION, + METADATA_JSON, + METADATA_JSON_URL) = range(20) def __init__(self, wrapper, pid_store): self.wrapper = wrapper @@ -98,6 +101,8 @@ def _ResetData(self): self.outstanding_pid = None self.work_state = None self.manufacturer_pids = [] + self.metadata_parameter_version_pids = [] + self.metadata_json_pids = [] self.slots = set() self.personalities = [] self.sensors = [] @@ -143,6 +148,15 @@ def _GetLanguage(self): return this_device.get('language', DEFAULT_LANGUAGE) return None + def _GetParameterMetadata(self, pid): + this_device = self._GetDevice() + if this_device: + this_version = self._GetVersion() + if this_version: + return this_version.setdefault('parameter_metadata', {}).setdefault( + pid, {}) + return None + def _CheckPidSupported(self, pid): this_version = self._GetVersion() if this_version: @@ -208,6 +222,12 @@ def _HandleResponse(self, unpacked_data): self._HandleProductURL(unpacked_data) elif self.work_state == self.FIRMWARE_URL: self._HandleFirmwareURL(unpacked_data) + elif self.work_state == self.METADATA_PARAMETER_VERSION: + self._HandleMetadataParameterVersion(unpacked_data) + elif self.work_state == self.METADATA_JSON: + self._HandleMetadataJSON(unpacked_data) + elif self.work_state == self.METADATA_JSON_URL: + self._HandleMetadataJSONURL(unpacked_data) def _HandleDeviceInfo(self, data): """Called when we get a DEVICE_INFO response.""" @@ -278,6 +298,9 @@ def _HandleSupportedParams(self, data): param_info['param_id'] <= RDMConstants.RDM_MANUFACTURER_PID_MAX): self.manufacturer_pids.append(param_info['param_id']) + # Duplicate the list of manufacturer PIDs for other processing + self.metadata_parameter_version_pids.extend(self.manufacturer_pids) + self.metadata_json_pids.extend(self.manufacturer_pids) self._NextState() def _HandleSoftwareVersionLabel(self, data): @@ -405,6 +428,27 @@ def _HandleFirmwareURL(self, data): this_device['firmware_url'] = data['url'] self._NextState() + def _HandleMetadataParameterVersion(self, data): + """Called when we get a METADATA_PARAMETER_VERSION response.""" + if data is not None: + this_param = self._GetParameterMetadata(data['pid']) + this_param['metadata_parameter_version'] = data['version'] + self._FetchNextMetadataParameterVersion() + + def _HandleMetadataJSON(self, data): + """Called when we get a METADATA_JSON response.""" + if data is not None: + this_param = self._GetParameterMetadata(data['pid']) + # We store the raw string here in case it's not valid JSON + this_param['metadata_json'] = data['json'], + self._FetchNextMetadataJSON() + + def _HandleMetadataJSONURL(self, data): + """Called when we get a METADATA_JSON_URL response.""" + this_device = self._GetDevice() + this_device['metadata_json_url'] = data['url'] + self._NextState() + def _NextState(self): """Move to the next state of information fetching.""" if self.work_state == self.EMPTYING_QUEUE: @@ -529,6 +573,22 @@ def _NextState(self): logging.debug("Skipping pid %s as it's not supported on this device" % pid) self._NextState() + elif self.work_state == self.FIRMWARE_URL: + self.work_state = self.METADATA_PARAMETER_VERSION + self._FetchNextMetadataParameterVersion() + elif self.work_state == self.METADATA_PARAMETER_VERSION: + self.work_state = self.METADATA_JSON + self._FetchNextMetadataJSON() + elif self.work_state == self.METADATA_JSON: + # fetch metadata JSON URL + self.work_state = self.METADATA_JSON_URL + pid = self.pid_store.GetName('METADATA_JSON_URL') + if self._CheckPidSupported(pid): + self._GetPid(pid) + else: + logging.debug("Skipping pid %s as it's not supported on this device" % + pid) + self._NextState() else: # this one is done, onto the next UID self._FetchNextUID() @@ -634,6 +694,42 @@ def _FetchNextSlotDescription(self): logging.debug('No more slots to fetch SLOT_DESCRIPTION for') self._NextState() + def _FetchNextMetadataParameterVersion(self): + """Fetch the info for the next metadata parameter version, or proceed to + the next state if there are none left. + """ + if self.metadata_parameter_version_pids: + manufacturer_pid = self.metadata_parameter_version_pids.pop(0) + pid = self.pid_store.GetName('METADATA_PARAMETER_VERSION') + self.rdm_api.Get(self.universe, + self.uid, + PidStore.ROOT_DEVICE, + pid, + self._RDMRequestComplete, + [manufacturer_pid]) + logging.debug('Sent METADATA_PARAMETER_VERSION request') + self.outstanding_pid = pid + else: + self._NextState() + + def _FetchNextMetadataJSON(self): + """Fetch the info for the next metadata JSON, or proceed to the next state + if there are none left. + """ + if self.metadata_json_pids: + manufacturer_pid = self.metadata_json_pids.pop(0) + pid = self.pid_store.GetName('METADATA_JSON') + self.rdm_api.Get(self.universe, + self.uid, + PidStore.ROOT_DEVICE, + pid, + self._RDMRequestComplete, + [manufacturer_pid]) + logging.debug('Sent METADATA_JSON request') + self.outstanding_pid = pid + else: + self._NextState() + def _FetchQueuedMessages(self): """Fetch messages until the queue is empty.""" pid = self.pid_store.GetName('QUEUED_MESSAGE') diff --git a/tools/rdm/TestDefinitions.py b/tools/rdm/TestDefinitions.py index e97b0f58b..b58555a83 100644 --- a/tools/rdm/TestDefinitions.py +++ b/tools/rdm/TestDefinitions.py @@ -21,18 +21,32 @@ from ola.OlaClient import OlaClient, RDMNack from ola.PidStore import ROOT_DEVICE -from ola.RDMConstants import (INTERFACE_HARDWARE_TYPE_ETHERNET, - RDM_INTERFACE_INDEX_MAX, RDM_INTERFACE_INDEX_MIN, - RDM_MANUFACTURER_PID_MAX, - RDM_MANUFACTURER_PID_MIN, - RDM_MANUFACTURER_SD_MAX, RDM_MANUFACTURER_SD_MIN, - RDM_MAX_DOMAIN_NAME_LENGTH, - RDM_MAX_HOSTNAME_LENGTH, - RDM_MAX_SERIAL_NUMBER_LENGTH, - RDM_MAX_STRING_LENGTH, - RDM_MAX_TEST_DATA_PATTERN_LENGTH, - RDM_MIN_HOSTNAME_LENGTH, - RDM_ZERO_FOOTPRINT_DMX_ADDRESS) +from ola.RDMConstants import ( + INTERFACE_HARDWARE_TYPE_ETHERNET, + RDM_ESTA_PID_MAX, RDM_ESTA_PID_MIN, + RDM_INTERFACE_INDEX_MAX, RDM_INTERFACE_INDEX_MIN, + RDM_MANUFACTURER_PID_MAX, RDM_MANUFACTURER_PID_MIN, + RDM_MANUFACTURER_SD_MAX, RDM_MANUFACTURER_SD_MIN, + RDM_MAX_DOMAIN_NAME_LENGTH, + RDM_MAX_HOSTNAME_LENGTH, + RDM_MAX_PARAM_DATA_LENGTH, + RDM_MAX_SERIAL_NUMBER_LENGTH, + RDM_MAX_STRING_LENGTH, + RDM_MAX_TEST_DATA_PATTERN_LENGTH, + RDM_MIN_HOSTNAME_LENGTH, + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE, + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED, + RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED, + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED, + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED, + RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED, + RDM_ZERO_FOOTPRINT_DMX_ADDRESS) from ola.StringUtils import StringEscape from ola.testing.rdm import TestMixins from ola.testing.rdm.ExpectedResults import (RDM_GET, RDM_SET, AckGetResult, @@ -600,7 +614,6 @@ def VerifyResult(self, response, fields): class GetMaxPacketSize(DeviceInfoTest, ResponderTestFixture): """Check if the responder can handle a packet of the maximum size.""" CATEGORY = TestCategory.ERROR_CONDITIONS - MAX_PDL = 231 PROVIDES = ['supports_max_sized_pdl'] def Test(self): @@ -610,16 +623,16 @@ def Test(self): self.AckGetResult(), # Some crazy devices continue to ack InvalidResponse( advisory='Responder returned an invalid response to a command with ' - 'PDL of %d' % self.MAX_PDL + 'PDL of %d' % RDM_MAX_PARAM_DATA_LENGTH ), TimeoutResult( advisory='Responder timed out to a command with PDL of %d' % - self.MAX_PDL), + RDM_MAX_PARAM_DATA_LENGTH), ]) # Incrementing list, so we can find out which bit we have where in memory # if it overflows data = b'' - for i in range(0, self.MAX_PDL): + for i in range(0, RDM_MAX_PARAM_DATA_LENGTH): data += b'%c' % i self.SendRawGet(ROOT_DEVICE, self.pid, data) @@ -641,13 +654,13 @@ def Test(self): return self._lower = 1 - self._upper = GetMaxPacketSize.MAX_PDL + self._upper = RDM_MAX_PARAM_DATA_LENGTH self.SendPacket() def SendPacket(self): if self._lower + 1 == self._upper: self.AddWarning('Max PDL supported is < %d, was %d' % - (GetMaxPacketSize.MAX_PDL, self._lower)) + (RDM_MAX_PARAM_DATA_LENGTH, self._lower)) self.Stop() return @@ -709,8 +722,8 @@ class GetSupportedParameters(ResponderTestFixture): """GET supported parameters.""" CATEGORY = TestCategory.CORE PID = 'SUPPORTED_PARAMETERS' - PROVIDES = ['manufacturer_parameters', 'supported_parameters', - 'acks_supported_parameters'] + PROVIDES = ['esta_parameters', 'manufacturer_parameters', + 'supported_parameters', 'acks_supported_parameters'] # Declaring support for any of these is a warning: MANDATORY_PIDS = ['SUPPORTED_PARAMETERS', @@ -769,6 +782,7 @@ def Test(self): def VerifyResult(self, response, fields): if not response.WasAcked(): + self.SetProperty('esta_parameters', []) self.SetProperty('manufacturer_parameters', []) self.SetProperty('supported_parameters', []) self.SetProperty('acks_supported_parameters', False) @@ -786,6 +800,7 @@ def VerifyResult(self, response, fields): banned_pids[pid.value] = pid supported_parameters = [] + esta_parameters = [] manufacturer_parameters = [] count_by_pid = {} @@ -803,9 +818,23 @@ def VerifyResult(self, response, fields): continue supported_parameters.append(param_id) - if (param_id >= RDM_MANUFACTURER_PID_MIN and - param_id <= RDM_MANUFACTURER_PID_MAX): + if (param_id >= RDM_ESTA_PID_MIN and + param_id <= RDM_ESTA_PID_MAX): + esta_parameters.append(param_id) + + pid = self.LookupPidValue(param_id) + if pid is None: + self.AddAdvisory( + 'PID 0x%04hx listed in supported parameters but not in the OLA PID ' + 'data. Either OLA is out of date or PID isn\'t a valid ESTA PID' % + param_id) + elif (param_id >= RDM_MANUFACTURER_PID_MIN and + param_id <= RDM_MANUFACTURER_PID_MAX): manufacturer_parameters.append(param_id) + else: + self.AddWarning('PID 0x%04hx listed in supported parameters but not ' + 'within the valid ESTA or manufacturer PID ranges' % + param_id) # Check for duplicate PIDs for pid, count in count_by_pid.items(): @@ -815,9 +844,11 @@ def VerifyResult(self, response, fields): self.AddAdvisory('%s listed %d times in supported parameters' % (pid_obj, count)) else: - self.AddAdvisory('PID 0x%hx listed %d times in supported parameters' % - (pid, count)) + self.AddAdvisory( + 'PID 0x%04hx listed %d times in supported parameters' % + (pid, count)) + self.SetProperty('esta_parameters', esta_parameters) self.SetProperty('manufacturer_parameters', manufacturer_parameters) self.SetProperty('supported_parameters', supported_parameters) @@ -826,6 +857,11 @@ def VerifyResult(self, response, fields): unsupported_pids = [] for pid_name in pid_names: pid = self.LookupPid(pid_name) + + if pid is None: + self.SetBroken('Failed to lookup info for PID %s' % pid_name) + return + if pid.value in supported_parameters: supported_pids.append(pid.name) else: @@ -1204,7 +1240,7 @@ def VerifyResult(self, response, fields): return if self.current_param != fields['pid']: - self.SetFailed('Request for pid 0x%hx returned pid 0x%hx' % + self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' % (self.current_param, fields['pid'])) if fields['type'] != 0: @@ -3000,7 +3036,7 @@ def VerifyResult(self, response, fields): StringEscape(fields['name']))) def CheckCondition(self, sensor_number, fields, lhs, predicate_str, rhs): - """Check for a condition and add a warning if it isn't true.""" + """Check for a condition and add an advisory if it isn't true.""" predicate = self.PREDICATE_DICT[predicate_str] if predicate(fields[lhs], fields[rhs]): self.AddAdvisory( @@ -8079,40 +8115,6 @@ class SetFirmwareURLWithData(TestMixins.UnsupportedSetWithDataMixin, PID = 'FIRMWARE_URL' -class AllSubDevicesGetMetadataJSONURL(TestMixins.AllSubDevicesGetMixin, - OptionalParameterTestFixture): - """Send a get METADATA_JSON_URL to ALL_SUB_DEVICES.""" - PID = 'METADATA_JSON_URL' - - -class GetMetadataJSONURL(TestMixins.GetURLMixin, - OptionalParameterTestFixture): - """GET the metadata JSON URL.""" - CATEGORY = TestCategory.RDM_INFORMATION - PID = 'METADATA_JSON_URL' - EXPECTED_FIELDS = ['url'] - # Extend the existing allowed schemas - ALLOWED_SCHEMAS = ['http', 'https', 'ftp'] - - -class GetMetadataJSONURLWithData(TestMixins.GetWithDataMixin, - OptionalParameterTestFixture): - """GET METADATA_JSON_URL with data.""" - PID = 'METADATA_JSON_URL' - - -class SetMetadataJSONURL(TestMixins.UnsupportedSetMixin, - OptionalParameterTestFixture): - """Attempt to SET METADATA_JSON_URL.""" - PID = 'METADATA_JSON_URL' - - -class SetMetadataJSONURLWithData(TestMixins.UnsupportedSetWithDataMixin, - OptionalParameterTestFixture): - """Attempt to SET METADATA_JSON_URL with data.""" - PID = 'METADATA_JSON_URL' - - class AllSubDevicesGetShippingLock(TestMixins.AllSubDevicesGetMixin, OptionalParameterTestFixture): """Send a get SHIPPING_LOCK to ALL_SUB_DEVICES.""" @@ -8223,8 +8225,7 @@ class GetTestDataPatternLengthMaxStringLength(TestMixins.GetTestDataMixin, class GetTestDataPatternLengthMaxPDL(TestMixins.GetTestDataMixin, OptionalParameterTestFixture): """GET TEST_DATA with a pattern length of the max PDL.""" - # TODO(Peter): Make this a constant - PATTERN_LENGTH = 231 + PATTERN_LENGTH = RDM_MAX_PARAM_DATA_LENGTH class GetTestDataPatternLengthMaxPatternLength(TestMixins.GetTestDataMixin, @@ -8258,6 +8259,246 @@ def Test(self): self.SendRawGet(ROOT_DEVICE, self.pid, data) +class SetTestDataLoopbackDataLengthZero(TestMixins.SetTestDataMixin, + OptionalParameterTestFixture): + """SET TEST_DATA with loopback data with a length of 0.""" + LOOPBACK_DATA_LENGTH = 0 + + +class SetTestDataLoopbackDataLengthOne(TestMixins.SetTestDataMixin, + OptionalParameterTestFixture): + """SET TEST_DATA with loopback data with a length of 1.""" + LOOPBACK_DATA_LENGTH = 1 + + +class SetTestDataLoopbackDataLengthMaxStringLength( + TestMixins.SetTestDataMixin, + OptionalParameterTestFixture): + """SET TEST_DATA with loopback data with the max string length.""" + LOOPBACK_DATA_LENGTH = RDM_MAX_STRING_LENGTH + + +class SetTestDataLoopbackDataLengthMaxPDL(TestMixins.SetTestDataMixin, + OptionalParameterTestFixture): + """SET TEST_DATA with loopback data with a length of the max PDL.""" + LOOPBACK_DATA_LENGTH = RDM_MAX_PARAM_DATA_LENGTH + + +class AllSubDevicesGetCommsStatusNSC(TestMixins.AllSubDevicesGetMixin, + OptionalParameterTestFixture): + """Send a get COMMS_STATUS_NSC to ALL_SUB_DEVICES.""" + PID = 'COMMS_STATUS_NSC' + + +class GetCommsStatusNSC(TestMixins.GetMixin, OptionalParameterTestFixture): + """GET COMMS_STATUS_NSC.""" + CATEGORY = TestCategory.NETWORK_MANAGEMENT + PID = 'COMMS_STATUS_NSC' + EXPECTED_FIELDS = ['supported_fields', + 'additive_checksum_of_most_recent_nsc_packet', + 'nsc_packet_count', + 'nsc_most_recent_slot_count', + 'nsc_minimum_slot_count', + 'nsc_maximum_slot_count', + 'nsc_error_count'] + PROVIDES = ['nsc_supported_fields'] + + PREDICATE_DICT = { + '==': operator.eq, + '<': operator.lt, + '>': operator.gt, + } + + def VerifyResult(self, response, fields): + # Call super to set provides etc + super(GetCommsStatusNSC, self).VerifyResult(response, fields) + if not response.WasAcked(): + return + + if self.CheckFieldSupport(fields, + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE + ) and ( + self.CheckFieldSupport(fields, + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE)): + self.CheckCondition(fields, + 'nsc_minimum_slot_count', + '>', + 'nsc_maximum_slot_count') + + if self.CheckFieldSupport( + fields, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE) and ( + self.CheckFieldSupport(fields, + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE)): + self.CheckCondition(fields, + 'nsc_most_recent_slot_count', + '<', + 'nsc_minimum_slot_count') + if self.CheckFieldSupport( + fields, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE) and ( + self.CheckFieldSupport(fields, + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE)): + self.CheckCondition(fields, + 'nsc_most_recent_slot_count', + '>', + 'nsc_maximum_slot_count') + + self.CheckFieldBlanking(fields, + 'additive_checksum_of_most_recent_nsc_packet', + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE, + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED) + self.CheckFieldBlanking(fields, + 'nsc_packet_count', + RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED) + self.CheckFieldBlanking( + fields, + 'nsc_most_recent_slot_count', + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED) + self.CheckFieldBlanking(fields, + 'nsc_minimum_slot_count', + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED) + self.CheckFieldBlanking(fields, + 'nsc_maximum_slot_count', + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED) + self.CheckFieldBlanking(fields, + 'nsc_error_count', + RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED) + + self.CheckPacketCount( + fields, + 'nsc_most_recent_slot_count', + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE) + self.CheckPacketCount( + fields, + 'nsc_minimum_slot_count', + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE) + self.CheckPacketCount( + fields, + 'nsc_maximum_slot_count', + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE) + + if fields['supported_fields'] & 0xc0: + self.AddWarning('Bits 7-6 in the supported fields are set') + + def CheckFieldSupport(self, fields, bit): + return fields['supported_fields'] & bit + + def CheckFieldBlanking(self, fields, field, bit, value): + """Check supported fields behaviour.""" + if self.CheckFieldSupport(fields, bit): + if fields[field] == value: + self.AddAdvisory( + 'Field %s set as supported, but value is the unsupported value ' + '0x%hx' % + (field, value)) + else: + if fields[field] != value: + self.AddAdvisory( + 'Field %s set as not supported, but value isn\'t the unsupported ' + 'value 0x%hx (got 0x%hx)' % + (field, value, fields[field])) + + def CheckPacketCount(self, fields, field, bit): + """Check packet count versus slot count behaviour.""" + if self.CheckFieldSupport( + fields, + RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE + ) and self.CheckFieldSupport( + fields, + bit): + if fields['nsc_packet_count'] == 0: + if fields[field] > 0: + self.AddAdvisory( + 'Field %s > 0 (got %d) despite packet count being zero' % + (field, fields[field])) + else: + if fields[field] == 0: + self.AddAdvisory( + 'Field %s is 0 despite packet count being non-zero (got %d)' % + (field, fields['nsc_packet_count'])) + + def CheckCondition(self, fields, lhs, predicate_str, rhs): + """Check for a condition and add an advisory if it isn't true.""" + predicate = self.PREDICATE_DICT[predicate_str] + if predicate(fields[lhs], fields[rhs]): + self.AddAdvisory( + '%s (%d) %s %s (%d)' % + (lhs, fields[lhs], predicate_str, rhs, fields[rhs])) + + +class GetCommsStatusNSCWithData(TestMixins.GetWithDataMixin, + OptionalParameterTestFixture): + """GET COMMS_STATUS_NSC with data.""" + PID = 'COMMS_STATUS_NSC' + + +class SetCommsStatusNSC(OptionalParameterTestFixture): + """SET COMMS_STATUS_NSC to reset the counters.""" + CATEGORY = TestCategory.NETWORK_MANAGEMENT + PID = 'COMMS_STATUS_NSC' + REQUIRES = ['nsc_supported_fields'] + + def Test(self): + self.AddIfSetSupported(self.AckSetResult(action=self.VerifySet)) + self.SendSet(ROOT_DEVICE, self.pid) + + def VerifySet(self): + expected_fields = { + 'supported_fields': self.Property('nsc_supported_fields'), + } + + self.AddExpectedField(expected_fields, + 'additive_checksum_of_most_recent_nsc_packet', + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE, + RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED) + self.AddExpectedField(expected_fields, + 'nsc_packet_count', + RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED) + self.AddExpectedField(expected_fields, + 'nsc_most_recent_slot_count', + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED) + self.AddExpectedField(expected_fields, + 'nsc_minimum_slot_count', + RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED) + self.AddExpectedField(expected_fields, + 'nsc_maximum_slot_count', + RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED) + self.AddExpectedField(expected_fields, + 'nsc_error_count', + RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE, + RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED) + + self.AddIfGetSupported(self.AckGetResult(field_values=expected_fields)) + self.SendGet(ROOT_DEVICE, self.pid) + + def AddExpectedField(self, fields, field, bit, value): + """Add expected field value depending on if it's supported.""" + if not self.Property('nsc_supported_fields') & bit: + # If not supported, expect the blank value + fields[field] = value + else: + # TODO(Peter): Deal with the fact there may have been a NSC packet in + # between set and get (advisory with a descriptive message is probably + # better) + fields[field] = 0 + + +class SetCommsStatusNSCWithData(TestMixins.SetWithDataMixin, + OptionalParameterTestFixture): + """Send a SET COMMS_STATUS_NSC command with unnecessary data.""" + PID = 'COMMS_STATUS_NSC' + + class AllSubDevicesGetListTags(TestMixins.AllSubDevicesGetMixin, OptionalParameterTestFixture): """Send a get LIST_TAGS to ALL_SUB_DEVICES.""" @@ -8336,7 +8577,7 @@ class AllSubDevicesGetCheckTag(TestMixins.AllSubDevicesGetMixin, OptionalParameterTestFixture): """Send a get CHECK_TAG to ALL_SUB_DEVICES.""" PID = 'CHECK_TAG' - DATA = [b'foo'] + DATA = ['foo'] class GetCheckTagWithNoData(TestMixins.GetWithNoDataMixin, @@ -8441,6 +8682,222 @@ class SetDeviceUnitNumberWithExtraData(TestMixins.SetWithDataMixin, DATA = b'foobar' +class AllSubDevicesGetMetadataParameterVersion(TestMixins.AllSubDevicesGetMixin, + OptionalParameterTestFixture): + """Send a get METADATA_PARAMETER_VERSION to ALL_SUB_DEVICES.""" + PID = 'METADATA_PARAMETER_VERSION' + DATA = [0x8001] + + +class GetMetadataParameterVersion(OptionalParameterTestFixture): + """Check that GET METADATA_PARAMETER_VERSION works for any manufacturer + params. + """ + CATEGORY = TestCategory.RDM_INFORMATION + PID = 'METADATA_PARAMETER_VERSION' + REQUIRES = ['manufacturer_parameters'] + + def Test(self): + self.params = self.Property('manufacturer_parameters')[:] + if len(self.params) == 0: + self.SetNotRun('No manufacturer params found') + # This case is tested in GetMetadataParameterVersionForNonManufacturerPid + return + self._GetParam() + + def _GetParam(self): + if len(self.params) == 0: + self.Stop() + return + + self.AddExpectedResults( + self.AckGetResult(action=self._GetParam)) + self.current_param = self.params.pop() + self.SendGet(ROOT_DEVICE, self.pid, [self.current_param]) + + def VerifyResult(self, response, fields): + if not response.WasAcked(): + return + + if self.current_param != fields['pid']: + self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' % + (self.current_param, fields['pid'])) + + +class GetMetadataParameterVersionForNonManufacturerPid( + OptionalParameterTestFixture): + """GET METADATA_PARAMETER_VERSION for a non-manufacturer pid.""" + CATEGORY = TestCategory.ERROR_CONDITIONS + PID = 'METADATA_PARAMETER_VERSION' + REQUIRES = ['manufacturer_parameters'] + + def Test(self): + device_info_pid = self.LookupPid('DEVICE_INFO') + results = [ + self.NackGetResult(RDMNack.NR_UNKNOWN_PID), + self.NackGetResult( + RDMNack.NR_DATA_OUT_OF_RANGE, + advisory='Metadata Parameter Version appears to be supported but no ' + 'manufacturer PIDs were declared'), + ] + if self.Property('manufacturer_parameters'): + results = self.NackGetResult(RDMNack.NR_DATA_OUT_OF_RANGE) + + self.AddExpectedResults(results) + self.SendGet(ROOT_DEVICE, self.pid, [device_info_pid.value]) + + +class GetMetadataParameterVersionWithNoData(TestMixins.GetWithNoDataMixin, + OptionalParameterTestFixture): + """GET METADATA_PARAMETER_VERSION with no argument given.""" + PID = 'METADATA_PARAMETER_VERSION' + + +class GetMetadataParameterVersionWithExtraData(TestMixins.GetWithDataMixin, + OptionalParameterTestFixture): + """GET METADATA_PARAMETER_VERSION with more than 2 bytes of data.""" + PID = 'METADATA_PARAMETER_VERSION' + DATA = b'foo' # TODO(peter): Ensure the first 2 bytes are sane/valid. + + +class SetMetadataParameterVersion(TestMixins.UnsupportedSetMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_PARAMETER_VERSION.""" + PID = 'METADATA_PARAMETER_VERSION' + + +class SetMetadataParameterVersionWithData( + TestMixins.UnsupportedSetWithDataMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_PARAMETER_VERSION with data.""" + PID = 'METADATA_PARAMETER_VERSION' + + +class AllSubDevicesGetMetadataJSON(TestMixins.AllSubDevicesGetMixin, + OptionalParameterTestFixture): + """Send a get METADATA_JSON to ALL_SUB_DEVICES.""" + PID = 'METADATA_JSON' + DATA = [0x8001] + + +class GetMetadataJSON(TestMixins.GetJSONMixin, + OptionalParameterTestFixture): + """Check that GET METADATA_JSON works for any manufacturer params.""" + CATEGORY = TestCategory.RDM_INFORMATION + PID = 'METADATA_JSON' + REQUIRES = ['manufacturer_parameters'] + # JSON first, as it's the field we want to do JSON validation on + EXPECTED_FIELDS = ['json', 'pid'] + + def Test(self): + self.params = self.Property('manufacturer_parameters')[:] + if len(self.params) == 0: + self.SetNotRun('No manufacturer params found') + # This case is tested in GetMetadataJSONForNonManufacturerPid + return + self._GetParam() + + def _GetParam(self): + if len(self.params) == 0: + self.Stop() + return + + self.AddExpectedResults( + self.AckGetResult(action=self._GetParam)) + self.current_param = self.params.pop() + self.SendGet(ROOT_DEVICE, self.pid, [self.current_param]) + + def VerifyResult(self, response, fields): + super(TestMixins.GetJSONMixin, self).VerifyResult(response, fields) + + if self.current_param != fields['pid']: + self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' % + (self.current_param, fields['pid'])) + + # TODO(Peter): Validate JSON PID field too + + +class GetMetadataJSONForNonManufacturerPid(OptionalParameterTestFixture): + """GET METADATA_JSON for a non-manufacturer pid.""" + CATEGORY = TestCategory.ERROR_CONDITIONS + PID = 'METADATA_JSON' + REQUIRES = ['manufacturer_parameters'] + + def Test(self): + device_info_pid = self.LookupPid('DEVICE_INFO') + results = [ + self.NackGetResult(RDMNack.NR_UNKNOWN_PID), + self.NackGetResult( + RDMNack.NR_DATA_OUT_OF_RANGE, + advisory='Metadata JSON appears to be supported but no ' + 'manufacturer PIDs were declared'), + ] + if self.Property('manufacturer_parameters'): + results = self.NackGetResult(RDMNack.NR_DATA_OUT_OF_RANGE) + + self.AddExpectedResults(results) + self.SendGet(ROOT_DEVICE, self.pid, [device_info_pid.value]) + + +class GetMetadataJSONWithNoData(TestMixins.GetWithNoDataMixin, + OptionalParameterTestFixture): + """GET METADATA_JSON with no argument given.""" + PID = 'METADATA_JSON' + + +class GetMetadataJSONWithExtraData(TestMixins.GetWithDataMixin, + OptionalParameterTestFixture): + """GET METADATA_JSON with more than 2 bytes of data.""" + PID = 'METADATA_JSON' + DATA = b'foo' # TODO(peter): Ensure the first 2 bytes are sane/valid. + + +class SetMetadataJSON(TestMixins.UnsupportedSetMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_JSON.""" + PID = 'METADATA_JSON' + + +class SetMetadataJSONWithData(TestMixins.UnsupportedSetWithDataMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_JSON with data.""" + PID = 'METADATA_JSON' + + +class AllSubDevicesGetMetadataJSONURL(TestMixins.AllSubDevicesGetMixin, + OptionalParameterTestFixture): + """Send a get METADATA_JSON_URL to ALL_SUB_DEVICES.""" + PID = 'METADATA_JSON_URL' + + +class GetMetadataJSONURL(TestMixins.GetURLMixin, + OptionalParameterTestFixture): + """GET the metadata JSON URL.""" + CATEGORY = TestCategory.RDM_INFORMATION + PID = 'METADATA_JSON_URL' + EXPECTED_FIELDS = ['url'] + # Extend the existing allowed schemas + ALLOWED_SCHEMAS = ['http', 'https', 'ftp'] + + +class GetMetadataJSONURLWithData(TestMixins.GetWithDataMixin, + OptionalParameterTestFixture): + """GET METADATA_JSON_URL with data.""" + PID = 'METADATA_JSON_URL' + + +class SetMetadataJSONURL(TestMixins.UnsupportedSetMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_JSON_URL.""" + PID = 'METADATA_JSON_URL' + + +class SetMetadataJSONURLWithData(TestMixins.UnsupportedSetWithDataMixin, + OptionalParameterTestFixture): + """Attempt to SET METADATA_JSON_URL with data.""" + PID = 'METADATA_JSON_URL' + + # E1.33/E1.37-7 PIDS # ============================================================================= diff --git a/tools/rdm/TestMixins.py b/tools/rdm/TestMixins.py index b5e08e9dd..15bb20626 100644 --- a/tools/rdm/TestMixins.py +++ b/tools/rdm/TestMixins.py @@ -15,6 +15,7 @@ # TestMixins.py # Copyright (C) 2010 Simon Newton +import json import struct import sys @@ -177,6 +178,12 @@ class GetURLMixin(GetMixin): # TODO(Peter): Make this a constant MAX_LENGTH = 231 ALLOWED_SCHEMAS = ['http', 'https'] + # TODO(Peter): Add non-English ones from https://en.wikipedia.org/wiki/.test + # From RFC 2606 and RFC 6762 + DENIED_TLDS = ['test', 'example', 'internal', 'invalid', 'local', + 'localhost'] + # From RFC 2606 + DENIED_DOMAINS = ['example.com', 'example.net', 'example.org'] def VerifyResult(self, response, fields): if not response.WasAcked(): @@ -225,12 +232,38 @@ def VerifyResult(self, response, fields): # TODO(Peter): Possibly check for ValueError locally here too... if url_result.netloc is None or not url_result.netloc: self.AddAdvisory( - '%s field in %s had no netloc' % - (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name)) - # else: - # TODO(Peter): Check for a dot in the netloc - # TODO(Peter): Check the netloc domain isn't a prohibited internal - # only one + '%s field in %s had no netloc, was %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, url_field)) + else: + if url_result.hostname is None or not url_result.hostname: + self.AddAdvisory( + '%s field in %s had no hostname, was %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + url_field)) + else: + if '.' not in url_result.hostname: + self.AddAdvisory( + '%s field in %s had hostname without a dot, expecting an ' + 'FQDN, was %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + url_result.hostname)) + + for tld in self.DENIED_TLDS: + tld_with_dot = "." + tld + if url_result.hostname.endswith(tld_with_dot): + self.AddAdvisory( + '%s field in %s had hostname ending with denied TLD %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + tld_with_dot)) + + for domain in self.DENIED_DOMAINS: + domain_with_dot = "." + domain + if (url_result.hostname is domain or + url_result.hostname.endswith(domain_with_dot)): + self.AddAdvisory( + '%s field in %s had hostname ending with denied domain %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + domain)) # TODO(Peter): Optionally expect at least one other section # (product/firmware) @@ -241,6 +274,57 @@ def VerifyResult(self, response, fields): url_field)) +class GetJSONMixin(GetMixin): + """GET Mixin for an optional JSON PID. Verify EXPECTED_FIELDS are in the + response. + + This mixin also sets a property if PROVIDES is defined. The target class + needs to defined EXPECTED_FIELDS and optionally PROVIDES. + """ + # Min length is based on simplest empty JSON of {} + MIN_LENGTH = 2 + # TODO(Peter): Max length is unlimited? + MAX_LENGTH = 255 + + def VerifyResult(self, response, fields): + if not response.WasAcked(): + return + + json_field = fields[self.EXPECTED_FIELDS[0]] + + if self.PROVIDES: + self.SetProperty(self.PROVIDES[0], json_field) + + if ContainsUnprintable(json_field): + self.AddAdvisory( + '%s field in %s contains unprintable characters, was %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + StringEscape(json_field))) + + if self.MIN_LENGTH and len(json_field) < self.MIN_LENGTH: + self.SetFailed( + '%s field in %s was shorter than expected, was %d, expected %d' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + len(json_field), self.MIN_LENGTH)) + + if self.MAX_LENGTH and len(json_field) > self.MAX_LENGTH: + self.SetFailed( + '%s field in %s was longer than expected, was %d, expected %d' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, + len(json_field), self.MAX_LENGTH)) + + # TODO(Peter): Do basic JSON validation + try: + parse_json = json.loads(json_field) + + # TODO(Peter): Add the option to do test-specific validation + except ValueError as err: + self.SetFailed( + '%s field in %s didn\'t parse as valid JSON due to %s, was %s' % + (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, str(err), + json_field)) + + class GetTestDataMixin(ResponderTestFixture): """GET TEST_DATA PID with a given pattern length. @@ -268,6 +352,38 @@ def Test(self): self.SendGet(PidStore.ROOT_DEVICE, self.pid, [self.PATTERN_LENGTH]) +class SetTestDataMixin(ResponderTestFixture): + """SET TEST_DATA PID with a given pattern length. + + If ALLOWED_NACKS is non-empty, this adds a custom NackGetResult to the list + of allowed results for each entry. + """ + PID = 'TEST_DATA' + CATEGORY = TestCategory.NETWORK_MANAGEMENT + LOOPBACK_DATA_LENGTH = 1 + ALLOWED_NACKS = [] + EXPECTED_FIELDS = ['loopback_data'] + + def Test(self): + expected_value = [] + for i in reversed(range(0, self.LOOPBACK_DATA_LENGTH)): + expected_value.append({'data': (i % (255 + 1))}) + results = [ + self.AckSetResult( + field_names=self.EXPECTED_FIELDS, + field_values={self.EXPECTED_FIELDS[0]: expected_value}) + ] + for nack in self.ALLOWED_NACKS: + results.append(self.NackSetResult(nack)) + self.AddIfSetSupported(results) + data = b'' + # Descending data to differentiate from GET TEST_DATA + for i in reversed(range(0, self.LOOPBACK_DATA_LENGTH)): + data += b'%c' % i + # TODO(Peter): using SendRawSet until we fix packing of groups in Python + self.SendRawSet(PidStore.ROOT_DEVICE, self.pid, data) + + class GetRequiredMixin(ResponderTestFixture): """GET Mixin for a required PID. Verify EXPECTED_FIELDS is in the response.