diff --git a/lib/include/kickcat/Mailbox.h b/lib/include/kickcat/Mailbox.h index 67ac3790..f167ee94 100644 --- a/lib/include/kickcat/Mailbox.h +++ b/lib/include/kickcat/Mailbox.h @@ -210,6 +210,14 @@ namespace kickcat::mailbox::response /// \brief Synchronous single-shot processing: dispatch, process, and return the reply std::vector processRequest(std::vector&& raw_message); + /// \brief Serve an ETG.8200 gateway request synchronously against this OD and return + /// a completed GatewayMessage carrying the reply, ready to be consumed by + /// Gateway::processPendingRequests(). Intended for the master-side (ETG.1510) path. + /// \return nullptr if the request is malformed (too small to contain a mailbox header) + /// or if the dictionary produced no/malformed reply. + std::shared_ptr createGatewayMessage( + uint8_t const* raw_message, int32_t raw_message_size, uint16_t gateway_index); + // Access on the next message to send: mainly for unit test std::vector const& readyToSend() const { return to_send_.front(); } diff --git a/lib/master/include/kickcat/Bus.h b/lib/master/include/kickcat/Bus.h index 00dd3b06..6f4d0fb4 100644 --- a/lib/master/include/kickcat/Bus.h +++ b/lib/master/include/kickcat/Bus.h @@ -149,6 +149,11 @@ namespace kickcat /// \return nullptr if message cannot be added (malformed, bad address, unsupported protocol, etc.), a handle on the message otherwise std::shared_ptr addGatewayMessage(uint8_t const* raw_message, int32_t raw_message_size, uint16_t gateway_index); + /// \brief Install a master-side response mailbox to serve ETG.1510 (address == 0) gateway requests. + /// \details The bus does not take ownership: the caller keeps the mailbox alive for as long as + /// the bus may route requests to it. Pass nullptr to detach. + void setMasterMailbox(mailbox::response::Mailbox* mbx) { master_mailbox_ = mbx; } + void clearErrorCounters(); // Helpers for broadcast commands, mainly for init purpose @@ -240,6 +245,8 @@ namespace kickcat Slave* dc_slave_{nullptr}; MailboxStatusFMMU mailbox_status_fmmu_{MailboxStatusFMMU::NONE}; + + mailbox::response::Mailbox* master_mailbox_{nullptr}; }; /** diff --git a/lib/master/include/kickcat/Slave.h b/lib/master/include/kickcat/Slave.h index 59030bd0..8d812056 100644 --- a/lib/master/include/kickcat/Slave.h +++ b/lib/master/include/kickcat/Slave.h @@ -15,6 +15,10 @@ namespace kickcat { void parseSII(uint8_t const* data, std::size_t size); + /// \brief Human-readable slave name. Returns the SII general-category device name when present, + /// otherwise a fallback derived from the fixed station address (e.g. "Slave @0x1001"). + std::string name() const; + ErrorCounters const& errorCounters() const; int computeErrorCounters() const; diff --git a/lib/master/src/Bus.cc b/lib/master/src/Bus.cc index 2da2791b..00f2a5f9 100644 --- a/lib/master/src/Bus.cc +++ b/lib/master/src/Bus.cc @@ -1331,12 +1331,16 @@ namespace kickcat mailbox::Header mbx_header; std::memcpy(&mbx_header, raw_message, sizeof(mailbox::Header)); - // Try to associate the request with a destination + // Try to associate the request with a destination. + // Per ETG.1510 section 6, address == 0 in an ETG.8200 request addresses the master OD. if (mbx_header.address == 0) { - // Master is the destination, unsupported for now (ETG 1510) - bus_error("Master mailbox not implemented"); - return nullptr; + if (master_mailbox_ == nullptr) + { + bus_error("Master mailbox not implemented"); + return nullptr; + } + return master_mailbox_->createGatewayMessage(raw_message, raw_message_size, gateway_index); } auto it = std::find_if(slaves_.begin(), slaves_.end(), [&](Slave const& slave) { return slave.address == mbx_header.address; }); diff --git a/lib/master/src/MasterOD.cc b/lib/master/src/MasterOD.cc index c01dd07a..7e03dfb6 100644 --- a/lib/master/src/MasterOD.cc +++ b/lib/master/src/MasterOD.cc @@ -72,27 +72,57 @@ namespace kickcat auto const& sii = slave.sii; uint16_t index = 0x8000 + i; + // Slave name: ETG.1510 :03 references the SII general category name string. + // Slave::name() handles the SII lookup and falls back to a station-address label. + std::string slave_name = slave.name(); + uint16_t name_bitlen = static_cast(slave_name.size() * 8); + + // ETG.1510 Table 9 rows :36 Link Preset / :37 Flags are ENI-derived. KickCAT has no ENI + // ingestion yet, so expose conformant zero defaults (no redundancy, no hot-connect group, + // no explicit expected physical links). Override once ENI lands. + uint8_t const link_preset = 0; + uint8_t const flags = 0; + CoE::Object obj{index, CoE::ObjectCode::RECORD, "Slave Configuration", {}}; - CoE::addEntry (obj, 0, 8, 0, CoE::Access::READ, CoE::DataType::UNSIGNED8, "Number of Entries", 7); - CoE::addEntry(obj, 1, 16, 8, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Fixed Station Address", slave.address); - CoE::addEntry(obj, 5, 32, 24, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Vendor Id", sii.info.vendor_id); - CoE::addEntry(obj, 6, 32, 56, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Product Code", sii.info.product_code); - CoE::addEntry(obj, 7, 32, 88, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Revision Number", sii.info.revision_number); - CoE::addEntry(obj, 8, 32, 120, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Serial Number", sii.info.serial_number); - CoE::addEntry(obj, 33, 16, 152, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Mailbox Out Size", sii.info.standard_recv_mbx_size); - CoE::addEntry(obj, 34, 16, 168, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Mailbox In Size", sii.info.standard_send_mbx_size); + + // Subindex 0 is the highest supported subindex (CANopen convention), not a count. + uint16_t bit_offset = 0; + CoE::addEntry (obj, 0, 8, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED8, "Number of Entries", uint8_t{39}); + bit_offset += 8; + CoE::addEntry(obj, 1, 16, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Fixed Station Address", slave.address); + bit_offset += 16; + // ETG.1510 Table 9 has no :02 entry; jump from :01 to :03. + CoE::addEntry(obj, 3, name_bitlen, bit_offset, CoE::Access::READ, CoE::DataType::VISIBLE_STRING, "Name", slave_name.c_str()); + bit_offset += name_bitlen; + CoE::addEntry(obj, 5, 32, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Vendor Id", sii.info.vendor_id); + bit_offset += 32; + CoE::addEntry(obj, 6, 32, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Product Code", sii.info.product_code); + bit_offset += 32; + CoE::addEntry(obj, 7, 32, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Revision Number", sii.info.revision_number); + bit_offset += 32; + CoE::addEntry(obj, 8, 32, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED32, "Serial Number", sii.info.serial_number); + bit_offset += 32; + CoE::addEntry(obj, 33, 16, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Mailbox Out Size", sii.info.standard_recv_mbx_size); + bit_offset += 16; + CoE::addEntry(obj, 34, 16, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Mailbox In Size", sii.info.standard_send_mbx_size); + bit_offset += 16; + CoE::addEntry (obj, 36, 8, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED8, "Link Preset", link_preset); + bit_offset += 8; + CoE::addEntry (obj, 37, 8, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED8, "Flags", flags); + bit_offset += 8; + CoE::addEntry(obj, 39, 16, bit_offset, CoE::Access::READ, CoE::DataType::UNSIGNED16, "Mailbox Protocols Supported", sii.info.mailbox_protocol); dict.push_back(std::move(obj)); auto& entries = dict.back().entries; ConfigurationData config; config.fixed_address = &entries[1]; - config.vendor_id = &entries[2]; - config.product_code = &entries[3]; - config.revision = &entries[4]; - config.serial_number = &entries[5]; - config.mailbox_out_size = &entries[6]; - config.mailbox_in_size = &entries[7]; + config.vendor_id = &entries[3]; + config.product_code = &entries[4]; + config.revision = &entries[5]; + config.serial_number = &entries[6]; + config.mailbox_out_size = &entries[7]; + config.mailbox_in_size = &entries[8]; configuration_data_.push_back(config); } diff --git a/lib/master/src/Slave.cc b/lib/master/src/Slave.cc index a0364ddb..44019c0a 100644 --- a/lib/master/src/Slave.cc +++ b/lib/master/src/Slave.cc @@ -1,4 +1,5 @@ #include +#include #include #include "Slave.h" @@ -22,6 +23,26 @@ namespace kickcat mailbox_bootstrap.send_size = sii.info.bootstrap_send_mbx_size; } + std::string Slave::name() const + { + uint8_t name_idx = sii.general.device_name_id; + if ((name_idx > 0) and (name_idx <= sii.strings.size())) + { + auto const& s = sii.strings[name_idx - 1]; + if (not s.empty()) + { + return s; + } + } + + // Fallback: derive a label from the fixed station address so operators can still correlate + // the slave with what they see on the wire. + char buf[16]; + std::snprintf(buf, sizeof(buf), "Slave @0x%04X", address); + return buf; + } + + ErrorCounters const& Slave::errorCounters() const { return error_counters; diff --git a/lib/src/Mailbox.cc b/lib/src/Mailbox.cc index 6c495e45..25fdba11 100644 --- a/lib/src/Mailbox.cc +++ b/lib/src/Mailbox.cc @@ -434,6 +434,40 @@ namespace kickcat::mailbox::response } + std::shared_ptr Mailbox::createGatewayMessage( + uint8_t const* raw_message, int32_t raw_message_size, uint16_t gateway_index) + { + // Guard against malformed or truncated requests before treating the buffer as a mailbox frame. + // Covers both negative sizes and "shorter than the mailbox header" sizes in one check. + if (raw_message_size < static_cast(sizeof(mailbox::Header))) + { + return nullptr; + } + + std::vector request(raw_message, raw_message + raw_message_size); + auto reply = processRequest(std::move(request)); + if (reply.size() < sizeof(mailbox::Header)) + { + // Empty or malformed reply (the dictionary or a factory produced nothing routable). + return nullptr; + } + + // Wrap the reply and drive it to SUCCESS by tagging its header with the gateway-masked + // address and feeding it back through GatewayMessage::process(). process() then restores + // the original (master) address on the delivered frame and flips status to SUCCESS, which + // is what Gateway::processPendingRequests() looks for. + uint16_t mailbox_size = static_cast( + std::max(reply.size(), static_cast(raw_message_size))); + auto msg = std::make_shared(mailbox_size, raw_message, gateway_index, 0ms); + + auto* reply_header = reinterpret_cast(reply.data()); + reply_header->address = mailbox::GATEWAY_MESSAGE_MASK | gateway_index; + msg->process(reply.data()); + + return msg; + } + + void Mailbox::send() { SyncManager sync; diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index 1b4142fb..3d0ad267 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/mailbox/CoE/request-t.cc src/mailbox/CoE/response-t.cc src/masterOD-t.cc + src/masterOD-gateway-t.cc src/slave/slave-t.cc src/slave/PDO-t.cc src/Time.cc diff --git a/unit/src/masterOD-gateway-t.cc b/unit/src/masterOD-gateway-t.cc new file mode 100644 index 00000000..c7dbbc52 --- /dev/null +++ b/unit/src/masterOD-gateway-t.cc @@ -0,0 +1,186 @@ +#include +#include + +#include "mocks/Link.h" +#include "mocks/Sockets.h" + +#include "kickcat/Bus.h" +#include "kickcat/Gateway.h" +#include "kickcat/Mailbox.h" +#include "kickcat/MasterOD.h" +#include "kickcat/CoE/mailbox/request.h" + +using namespace kickcat; +using ::testing::_; + +constexpr uint16_t MBX_SIZE = 256; + +static MasterIdentity testIdentity() +{ + MasterIdentity id; + id.device_type = 0x12345678; + id.vendor_id = 0xAABBCCDD; + id.product_code = 0x11223344; + id.revision = 0x00000001; + id.serial_number = 0xDEADBEEF; + return id; +} + + +class MasterGatewayTest : public testing::Test +{ +public: + void SetUp() override + { + MasterOD od(testIdentity()); + mbx.enableCoE(od.createDictionary()); + bus.setMasterMailbox(&mbx); + } + + std::shared_ptr link{std::make_shared()}; + mailbox::response::Mailbox mbx{MBX_SIZE}; + Bus bus{link}; +}; + + +TEST_F(MasterGatewayTest, address_zero_reads_master_identity) +{ + // SDO upload 0x1018:01 (Vendor ID) with the default mailbox header address == 0 targets the master OD. + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x1018, 1, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + ASSERT_EQ(0u, sdo_msg.address()); + + auto gw_msg = bus.addGatewayMessage(sdo_msg.data(), static_cast(sdo_msg.size()), 42); + ASSERT_NE(nullptr, gw_msg); + EXPECT_EQ(42u, gw_msg->gatewayIndex()); + EXPECT_EQ(mailbox::request::MessageStatus::SUCCESS, gw_msg->status()); + + auto const* reply_header = pointData(gw_msg->data()); + auto const* coe = pointData(reply_header); + auto const* sdo = pointData(coe); + auto const* payload = pointData(sdo); + + EXPECT_EQ(mailbox::Type::CoE, reply_header->type); + EXPECT_EQ(0u, reply_header->address); // master address, restored by GatewayMessage::process() + EXPECT_EQ(CoE::Service::SDO_RESPONSE, coe->service); + EXPECT_EQ(CoE::SDO::response::UPLOAD, sdo->command); + EXPECT_EQ(testIdentity().vendor_id, *payload); +} + + +TEST_F(MasterGatewayTest, unknown_slave_address_still_returns_nullptr_when_master_mailbox_set) +{ + // Regression: installing a master mailbox must not reroute slave-addressed requests to it. + // Unknown slave addresses continue to fall through to the "no slave on the bus" error path. + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x1018, 1, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + sdo_msg.setAddress(0x1234); + + EXPECT_EQ(nullptr, bus.addGatewayMessage(sdo_msg.data(), static_cast(sdo_msg.size()), 1)); +} + + +TEST_F(MasterGatewayTest, address_zero_unknown_object_returns_sdo_abort) +{ + // Unknown object must come back as an SDO abort, not as nullptr: the master mailbox owns + // the error reply and the gateway path must deliver it to the client. + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x9999, 0, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + + auto gw_msg = bus.addGatewayMessage(sdo_msg.data(), static_cast(sdo_msg.size()), 7); + ASSERT_NE(nullptr, gw_msg); + EXPECT_EQ(mailbox::request::MessageStatus::SUCCESS, gw_msg->status()); + + auto const* coe = pointData(pointData(gw_msg->data())); + auto const* sdo = pointData(coe); + auto const* payload = pointData(sdo); + + EXPECT_EQ(CoE::Service::SDO_REQUEST, coe->service); + EXPECT_EQ(CoE::SDO::request::ABORT, sdo->command); + EXPECT_EQ(CoE::SDO::abort::OBJECT_DOES_NOT_EXIST, *payload); +} + + +TEST(MasterGatewayNoMailbox, address_zero_without_master_mailbox_returns_nullptr) +{ + // Regression: with no master mailbox registered, the legacy "not implemented" error path is kept. + auto link = std::make_shared(); + Bus bus{link}; + + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x1018, 1, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + + EXPECT_EQ(nullptr, bus.addGatewayMessage(sdo_msg.data(), static_cast(sdo_msg.size()), 1)); +} + + +TEST(MasterGatewayNoMailbox, unknown_slave_address_still_returns_nullptr) +{ + // Regression: unknown slave addresses return nullptr regardless of master mailbox state. + auto link = std::make_shared(); + Bus bus{link}; + + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x1018, 1, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + sdo_msg.setAddress(0x1234); + + EXPECT_EQ(nullptr, bus.addGatewayMessage(sdo_msg.data(), static_cast(sdo_msg.size()), 1)); +} + + +TEST_F(MasterGatewayTest, full_udp_loop_through_gateway) +{ + // End-to-end: fake a UDP client sending an ETG.8200 frame targeting the master OD (address=0), + // run it through Gateway::fetchRequest() + Bus::addGatewayMessage() + Gateway::processPendingRequests(), + // and verify the reply comes back out of the same socket. + auto socket = std::make_shared(); + using namespace std::placeholders; + Gateway gateway{socket, std::bind(&Bus::addGatewayMessage, &bus, _1, _2, _3)}; + + constexpr uint16_t GATEWAY_INDEX = 5; + + uint32_t data{0}; + uint32_t data_size = sizeof(data); + mailbox::request::SDOMessage sdo_msg{MBX_SIZE, 0x1018, 2, false, CoE::SDO::request::UPLOAD, &data, &data_size, 1ms}; + + std::vector udp_frame(sizeof(EthercatHeader) + sdo_msg.size()); + auto* eth_header = reinterpret_cast(udp_frame.data()); + eth_header->type = EthercatType::MAILBOX; + eth_header->len = sdo_msg.size() & 0x7ff; + std::memcpy(udp_frame.data() + sizeof(EthercatHeader), sdo_msg.data(), sdo_msg.size()); + + EXPECT_CALL(*socket, recv(_, _)) + .WillOnce([&](void* frame, int32_t frame_size) -> std::tuple + { + EXPECT_GE(frame_size, static_cast(udp_frame.size())); + std::memcpy(frame, udp_frame.data(), udp_frame.size()); + return {static_cast(udp_frame.size()), GATEWAY_INDEX}; + }); + + EXPECT_CALL(*socket, sendTo(_, _, GATEWAY_INDEX)) + .WillOnce([&](void const* frame, int32_t size, uint16_t) -> int32_t + { + auto const* hdr = reinterpret_cast(frame); + EXPECT_EQ(EthercatType::MAILBOX, hdr->type); + + auto const* mbx_hdr = reinterpret_cast( + static_cast(frame) + sizeof(EthercatHeader)); + auto const* coe = pointData(mbx_hdr); + auto const* sdo = pointData(coe); + auto const* payload = pointData(sdo); + + EXPECT_EQ(0u, mbx_hdr->address); + EXPECT_EQ(CoE::Service::SDO_RESPONSE, coe->service); + EXPECT_EQ(CoE::SDO::response::UPLOAD, sdo->command); + EXPECT_EQ(testIdentity().product_code, *payload); + return size; + }); + + gateway.fetchRequest(); + gateway.processPendingRequests(); +} diff --git a/unit/src/masterOD-t.cc b/unit/src/masterOD-t.cc index a165d7e5..f58046b0 100644 --- a/unit/src/masterOD-t.cc +++ b/unit/src/masterOD-t.cc @@ -339,3 +339,104 @@ TEST(MasterODPopulate, entry_pointers_survive_dictionary_move) ASSERT_EQ(0x11u, *static_cast(configs[0].vendor_id->data)); ASSERT_EQ(0x22u, *static_cast(configs[1].vendor_id->data)); } + + +TEST(MasterODPopulate, number_of_entries_is_highest_supported_subindex) +{ + // ETG.1510 0x8nnn goes up to :39 when mandatory entries are populated. + // CANopen RECORD convention: subindex 0 reports the highest supported subindex. + MasterIdentity id; + MasterOD od(id); + + std::vector slaves(1); + auto dict = od.createDictionary(); + od.populate(dict, slaves); + + mailbox::response::Mailbox mbx{MBX_SIZE}; + mbx.enableCoE(std::move(dict)); + + auto reply = mbx.processRequest(buildSDOUpload(0x8000, 0)); + ASSERT_FALSE(reply.empty()); + auto* coe = pointData(pointData(reply.data())); + auto* sdo = pointData(coe); + ASSERT_EQ(CoE::Service::SDO_RESPONSE, coe->service); + ASSERT_EQ(39u, *pointData(sdo)); +} + + +TEST(MasterODPopulate, mandatory_subindices_from_spec) +{ + // Mandatory per ETG.1510 Table 9 beyond what PR2 landed: :03 Name, :36 Link Preset, + // :37 Flags, :39 Mailbox Protocols Supported. + MasterIdentity id; + MasterOD od(id); + + std::vector slaves(1); + // Point device_name_id at the first SII string (1-based index per ETG.2010). + slaves[0].sii.strings = {"Acme Drive"}; + slaves[0].sii.general.device_name_id = 1; + slaves[0].sii.info.mailbox_protocol = eeprom::MailboxProtocol::CoE | eeprom::MailboxProtocol::FoE; + + auto dict = od.createDictionary(); + od.populate(dict, slaves); + + mailbox::response::Mailbox mbx{MBX_SIZE}; + mbx.enableCoE(std::move(dict)); + + // :03 Name + auto reply_name = mbx.processRequest(buildSDOUpload(0x8000, 3)); + ASSERT_FALSE(reply_name.empty()); + auto* coe_name = pointData(pointData(reply_name.data())); + ASSERT_EQ(CoE::Service::SDO_RESPONSE, coe_name->service); + ASSERT_EQ("Acme Drive", extractStringFromSDOReply(reply_name)); + + // :36 Link Preset (default 0 until ENI is wired in) + auto reply_lp = mbx.processRequest(buildSDOUpload(0x8000, 36)); + ASSERT_FALSE(reply_lp.empty()); + auto* coe_lp = pointData(pointData(reply_lp.data())); + auto* sdo_lp = pointData(coe_lp); + ASSERT_EQ(CoE::Service::SDO_RESPONSE, coe_lp->service); + ASSERT_EQ(0u, *pointData(sdo_lp)); + + // :37 Flags (default 0 until ENI is wired in) + auto reply_fl = mbx.processRequest(buildSDOUpload(0x8000, 37)); + ASSERT_FALSE(reply_fl.empty()); + auto* coe_fl = pointData(pointData(reply_fl.data())); + auto* sdo_fl = pointData(coe_fl); + ASSERT_EQ(CoE::Service::SDO_RESPONSE, coe_fl->service); + ASSERT_EQ(0u, *pointData(sdo_fl)); + + // :39 Mailbox Protocols Supported (maps directly from SII mailbox_protocol bitmask) + auto reply_mp = mbx.processRequest(buildSDOUpload(0x8000, 39)); + ASSERT_FALSE(reply_mp.empty()); + auto* coe_mp = pointData(pointData(reply_mp.data())); + auto* sdo_mp = pointData(coe_mp); + ASSERT_EQ(CoE::Service::SDO_RESPONSE, coe_mp->service); + ASSERT_EQ(static_cast(eeprom::MailboxProtocol::CoE | eeprom::MailboxProtocol::FoE), + *pointData(sdo_mp)); +} + + +TEST(MasterODPopulate, name_falls_back_when_sii_has_no_string) +{ + // With no SII name string, :03 should still exist (spec marks it mandatory) with a sensible default + // derived from the fixed station address so operators can correlate entries with the bus. + MasterIdentity id; + MasterOD od(id); + + std::vector slaves(2); + slaves[0].address = 0x1001; + slaves[1].address = 0x100A; + // slaves[i].sii.general.device_name_id stays at 0 (no name) + + auto dict = od.createDictionary(); + od.populate(dict, slaves); + + mailbox::response::Mailbox mbx{MBX_SIZE}; + mbx.enableCoE(std::move(dict)); + + auto reply0 = mbx.processRequest(buildSDOUpload(0x8000, 3)); + auto reply1 = mbx.processRequest(buildSDOUpload(0x8001, 3)); + ASSERT_EQ("Slave @0x1001", extractStringFromSDOReply(reply0)); + ASSERT_EQ("Slave @0x100A", extractStringFromSDOReply(reply1)); +}