From 5972844ace77f7a7abe2c992f5f5cecf67217999 Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Mon, 1 Jan 2024 13:08:13 +0100 Subject: [PATCH 01/16] Fix compile error on FdUtils under freertos. --- src/utils/FdUtils.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/FdUtils.cxx b/src/utils/FdUtils.cxx index e9dc07dcf..4f5748005 100644 --- a/src/utils/FdUtils.cxx +++ b/src/utils/FdUtils.cxx @@ -34,10 +34,12 @@ #include "FdUtils.hxx" +#ifdef __linux__ #include #include #include #include /* tc* functions */ +#endif #include "nmranet_config.h" From ad0681b3328c5e95a74257f428d5a0d203e479c6 Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Sat, 6 Jan 2024 22:32:23 +0100 Subject: [PATCH 02/16] Fix test flakiness. --- src/openlcb/HubLatency.cxxtest | 1 + 1 file changed, 1 insertion(+) diff --git a/src/openlcb/HubLatency.cxxtest b/src/openlcb/HubLatency.cxxtest index 84f654a08..3e7cb2bf1 100644 --- a/src/openlcb/HubLatency.cxxtest +++ b/src/openlcb/HubLatency.cxxtest @@ -216,6 +216,7 @@ protected: LOG(INFO, "set up test case"); stack1_.start_executor_thread("stack1", 0, 0); stack1_.start_tcp_hub_server(50989); + wait_for_main_executor(); int fd = ConnectSocket("localhost", 50989); HASSERT(fd >= 0); create_gc_port_for_can_hub(&canHub2_, fd); From 5c1f88aeb048e186c99c4375880802b2af2cab29 Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Sat, 27 Jan 2024 17:06:57 +0100 Subject: [PATCH 03/16] Adds local loopback to TractionThrottle. (#756) This PR improves behavior when there is more than one TractionThrottle in the same openlcb::node_. Specifically, when these are assigned to the same dst_ locomotive, the Train Node will never send back echoes of the packets. This means that the simultaneous updates are missing. This PR makes all TractionThrottle objects lined up in a linked list upon creation. When a locomotive control commands (speed, estop, fn) is sent out, a local loopback will walk the linked list and identify if there are any other tractionthrottle objects assigned to the same loco. If so, the message will be handed over to it for update callbacks. === * Adds local loopback to TractionThrottle. This PR improves behavior when there is more than one TractionThrottle in the same openlcb::node_. Specifically, when these are assigned to the same dst_ locomotive, the Train Node will never send back echoes of the packets. This means that the simultaneous updates are missing. This PR makes all TractionThrottle objects lined up in a linked list upon creation. When a locomotive control commands (speed, estop, fn) is sent out, a local loopback will walk the linked list and identify if there are any other tractionthrottle objects assigned to the same loco. If so, the message will be handed over to it for update callbacks. * Remove unnecessary verbose logging. * Adds extra nodes to the test fixture. Removes repeated boilerplate for adding the second train node. * Adds a test for multiple throttle objects listening to the same train. * Adds more test cases to the local listener feedback. Ensures that the listener link doesn't get prematurely removed. When more than one throttle is listening to the same train node, any throttle would remove the listener link, leaving the others without a registration. Now only the last throttle remaining will remove the link. * Adds test coverage for the last throttle removing the consist link. * Makes sure we hold on to a ref while sending the packet to the send flow. --- src/openlcb/TractionClient.hxx | 14 +- src/openlcb/TractionThrottle.cxxtest | 202 +++++++++++++++++++++++---- src/openlcb/TractionThrottle.hxx | 192 ++++++++++++++++++++++--- 3 files changed, 354 insertions(+), 54 deletions(-) diff --git a/src/openlcb/TractionClient.hxx b/src/openlcb/TractionClient.hxx index 70b05ac66..7319dfd9e 100644 --- a/src/openlcb/TractionClient.hxx +++ b/src/openlcb/TractionClient.hxx @@ -153,16 +153,16 @@ private: Action entry() OVERRIDE { - LOG_ERROR("response came"); + LOG(VERBOSE, "response came"); if (!trigger_) { // We already matched -- drop all packets to the floor. - LOG_ERROR("no trigger"); + LOG(VERBOSE, "no trigger"); return release_and_exit(); } if (nmsg()->dstNode != expectedDst_) { - LOG_ERROR("dst not match"); + LOG(VERBOSE, "dst not match"); return release_and_exit(); } /// @TODO(balazs.racz) factor out this code into a helper function that @@ -172,7 +172,7 @@ private: { if (expectedSrc_.id != nmsg()->src.id) { - LOG_ERROR("src.id not match"); + LOG(VERBOSE, "src.id not match"); return release_and_exit(); } } @@ -180,7 +180,7 @@ private: { if (expectedSrc_.alias != nmsg()->src.alias) { - LOG_ERROR("src.alias not match"); + LOG(VERBOSE, "src.alias not match"); return release_and_exit(); } } @@ -194,11 +194,11 @@ private: // Now: message is from the right source node, to the right destination // node. if (nmsg()->payload.size() < 1) { - LOG_ERROR("no payload"); + LOG(VERBOSE, "no payload"); return release_and_exit(); } if (nmsg()->payload[0] != expectedType_) { - LOG_ERROR("payload type no match"); + LOG(VERBOSE, "payload type no match"); return release_and_exit(); } // Now: we matched! diff --git a/src/openlcb/TractionThrottle.cxxtest b/src/openlcb/TractionThrottle.cxxtest index b425afdf8..fe158b23c 100644 --- a/src/openlcb/TractionThrottle.cxxtest +++ b/src/openlcb/TractionThrottle.cxxtest @@ -8,6 +8,7 @@ namespace openlcb { static constexpr NodeID TRAIN_NODE_ID = 0x06010000C000 | 1372; +static constexpr NodeID REMOTE_NODE_ID = 0x0501010118F3ull; class ThrottleTest : public AsyncNodeTest { @@ -15,15 +16,29 @@ protected: ThrottleTest() { print_all_packets(); - run_x( - [this]() { otherIf_.local_aliases()->add(TRAIN_NODE_ID, 0x771); }); + run_x([this]() { + otherIf_.local_aliases()->add(TRAIN_NODE_ID, 0x771); + otherIf_.local_aliases()->add(TRAIN_NODE_ID+1, 0x772); + otherIf_.local_aliases()->add(REMOTE_NODE_ID, 0x553); + }); trainNode_.reset(new TrainNodeForProxy(&trainService_, &trainImpl_)); + trainNode2_.reset(new TrainNodeForProxy(&trainService_, &trainImpl2_)); + remoteNode_.reset(new DefaultNode(&otherIf_, REMOTE_NODE_ID, true)); + wait(); + // This will re-fill all the alias caches. + send_packet(":X19490559N;"); + send_packet(":X10702559N;"); wait(); } LoggingTrain trainImpl_{1372}; std::unique_ptr trainNode_; + LoggingTrain trainImpl2_{1373}; + std::unique_ptr trainNode2_; + + std::unique_ptr remoteNode_; + IfCan otherIf_{&g_executor, &can_hub0, 5, 5, 5}; TrainService trainService_{&otherIf_}; @@ -423,22 +438,16 @@ TEST_F(ThrottleClientTest, ReassignWithListener) EXPECT_EQ(1, trainNode_->query_consist_length()); EXPECT_QRYCONSIST(node_->node_id(), 0x8C, 0); - static auto TRAIN_NODE_ID2 = TRAIN_NODE_ID + 1; - run_x([this]() { otherIf_.local_aliases()->add(TRAIN_NODE_ID2, 0x772); }); - LoggingTrain train_impl2{1373}; - TrainNodeForProxy node2(&trainService_, &train_impl2); - wait(); - b = invoke_flow(&throttle_, TractionThrottleCommands::ASSIGN_TRAIN, - TRAIN_NODE_ID2, true); + TRAIN_NODE_ID + 1, true); ASSERT_EQ(0, b->data()->resultCode); EXPECT_EQ(0, trainNode_->query_consist_length()); - EXPECT_EQ(1, node2.query_consist_length()); + EXPECT_EQ(1, trainNode2_->query_consist_length()); b = invoke_flow(&throttle_, TractionThrottleCommands::RELEASE_TRAIN); ASSERT_EQ(0, b->data()->resultCode); EXPECT_EQ(0, trainNode_->query_consist_length()); - EXPECT_EQ(0, node2.query_consist_length()); + EXPECT_EQ(0, trainNode2_->query_consist_length()); wait(); } @@ -453,17 +462,11 @@ TEST_F(ThrottleClientTest, ReassignWithoutListener) EXPECT_EQ(1, trainNode_->query_consist_length()); - static auto TRAIN_NODE_ID2 = TRAIN_NODE_ID + 1; - run_x([this]() { otherIf_.local_aliases()->add(TRAIN_NODE_ID2, 0x772); }); - LoggingTrain train_impl2{1373}; - TrainNodeForProxy node2(&trainService_, &train_impl2); - wait(); - b = invoke_flow(&throttle_, TractionThrottleCommands::ASSIGN_TRAIN, - TRAIN_NODE_ID2, false); + TRAIN_NODE_ID + 1, false); ASSERT_EQ(0, b->data()->resultCode); EXPECT_EQ(0, trainNode_->query_consist_length()); - EXPECT_EQ(0, node2.query_consist_length()); + EXPECT_EQ(0, trainNode2_->query_consist_length()); wait(); } @@ -521,12 +524,6 @@ TEST_F(ThrottleClientTest, HeartbeatWrongTrain) TRAIN_NODE_ID, false); ASSERT_EQ(0, b->data()->resultCode); - static auto TRAIN_NODE_ID2 = TRAIN_NODE_ID + 1; - run_x([this]() { otherIf_.local_aliases()->add(TRAIN_NODE_ID2, 0x772); }); - LoggingTrain train_impl2{1373}; - TrainNodeForProxy node2(&trainService_, &train_impl2); - wait(); - // Primes the caches. openlcb::send_message(trainNode_.get(), Defs::MTI_TRACTION_CONTROL_COMMAND, NodeHandle(node_->node_id()), TractionDefs::fn_set_payload(13, 1)); @@ -536,7 +533,7 @@ TEST_F(ThrottleClientTest, HeartbeatWrongTrain) // heartbeat request is sent from wrong train to throttle expect_packet(":X191E9772N022A400303;"); // no response from the throttle. - openlcb::send_message(&node2, Defs::MTI_TRACTION_CONTROL_REPLY, + openlcb::send_message(trainNode2_.get(), Defs::MTI_TRACTION_CONTROL_REPLY, NodeHandle(node_->node_id()), TractionDefs::heartbeat_request_payload()); wait(); @@ -627,4 +624,157 @@ TEST_F(ThrottleClientTest, ListenerCallback) EXPECT_FALSE(throttle_.get_emergencystop()); } + +TEST_F(ThrottleClientTest, MultiListener) +{ + TractionThrottle throttle2{node_}; + TractionThrottle throttler{remoteNode_.get()}; + StrictMock l, l2, lr; + + throttle_.set_throttle_listener( + std::bind(&ListenerInterface::update, &l, std::placeholders::_1)); + throttle2.set_throttle_listener( + std::bind(&ListenerInterface::update, &l2, std::placeholders::_1)); + throttler.set_throttle_listener( + std::bind(&ListenerInterface::update, &lr, std::placeholders::_1)); + + auto b = invoke_flow(&throttle_, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID, true); + ASSERT_EQ(0, b->data()->resultCode); + b = invoke_flow(&throttle2, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID, true); + ASSERT_EQ(0, b->data()->resultCode); + b = invoke_flow(&throttler, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID, true); + ASSERT_EQ(0, b->data()->resultCode); + + wait(); + + // This message will trigger no callback on the bus, but the local feedback + // will make it appear. The remote node will get its own feedback. + EXPECT_CALL(l, update(23)); + EXPECT_CALL(lr, update(23)); + throttle2.set_fn(23, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // If the remote node calls, one message will come on the bus but both + // throttles will call update. + EXPECT_CALL(l, update(22)); + EXPECT_CALL(l2, update(22)); + throttler.set_fn(22, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // If a third node calls, all three will call update. + EXPECT_CALL(l, update(15)); + EXPECT_CALL(l2, update(15)); + EXPECT_CALL(lr, update(15)); + send_packet(":X195EB330N07710100000f0001;"); // fn 15 = 1 + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + LOG(INFO, "switching throttle2 to other train."); + + // ================= + // Local second throttle will be switched over to a different train. + b = invoke_flow(&throttle2, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID + 1, true); + ASSERT_EQ(0, b->data()->resultCode); + + // Now a third node calls, two shall call update. + EXPECT_CALL(l, update(14)); + EXPECT_CALL(lr, update(14)); + send_packet(":X195EB330N07710100000e0001;"); // fn 14 = 1 + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // When the remote node calls, the local shall update. + EXPECT_CALL(l, update(13)); + throttler.set_fn(13, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // Local2 calls go to a different train, no update callbacks. + throttle2.set_fn(13, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // Local1 calls go to the remote throttle, not to local2. + EXPECT_CALL(lr, update(12)); + throttle_.set_fn(12, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + EXPECT_EQ(2, trainNode_->query_consist_length()); + EXPECT_EQ(1, trainNode2_->query_consist_length()); + + // ================= + LOG(INFO, "switching throttle2 back but no listener."); + // Local second throttle reassigned to the same train but without listener. + b = invoke_flow(&throttle2, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID, false); + ASSERT_EQ(0, b->data()->resultCode); + + EXPECT_EQ(2, trainNode_->query_consist_length()); + EXPECT_EQ(0, trainNode2_->query_consist_length()); + + // Local1 calls go to the remote throttle, not to local2. + EXPECT_CALL(lr, update(11)); + throttle_.set_fn(11, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // Local2 goes to local1 and remote + EXPECT_CALL(l, update(23)); + EXPECT_CALL(lr, update(23)); + throttle2.set_fn(23, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // If the remote node calls, only local1 will get it. + EXPECT_CALL(l, update(22)); + throttler.set_fn(22, 1); + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + // If a third node calls, all local1 and remote will get it. + EXPECT_CALL(l, update(15)); + EXPECT_CALL(lr, update(15)); + send_packet(":X195EB330N07710100000f0001;"); // fn 15 = 1 + wait(); + Mock::VerifyAndClear(&l); + Mock::VerifyAndClear(&l2); + Mock::VerifyAndClear(&lr); + + EXPECT_EQ(2, trainNode_->query_consist_length()); + + // Local first throttle reassigned to the second train. + b = invoke_flow(&throttle_, TractionThrottleCommands::ASSIGN_TRAIN, + TRAIN_NODE_ID + 1, false); + ASSERT_EQ(0, b->data()->resultCode); + // removed listener. + EXPECT_EQ(1, trainNode_->query_consist_length()); +} + } // namespace openlcb diff --git a/src/openlcb/TractionThrottle.hxx b/src/openlcb/TractionThrottle.hxx index f5d271bc8..73bfa6b06 100644 --- a/src/openlcb/TractionThrottle.hxx +++ b/src/openlcb/TractionThrottle.hxx @@ -40,6 +40,7 @@ #include "openlcb/TractionDefs.hxx" #include "openlcb/TractionThrottleInterface.hxx" #include "openlcb/TrainInterface.hxx" +#include "utils/LinkedObject.hxx" namespace openlcb { @@ -47,7 +48,8 @@ namespace openlcb /** Interface for a single throttle for running a train node. * */ -class TractionThrottle : public TractionThrottleBase +class TractionThrottle : public TractionThrottleBase, + public LinkedObject { public: /// @param node is the openlcb node from which this throttle will be @@ -79,7 +81,8 @@ public: void set_speed(SpeedType speed) override { - send_traction_message(TractionDefs::speed_set_payload(speed)); + send_traction_message_with_loopback( + TractionDefs::speed_set_payload(speed)); lastSetSpeed_ = speed; estopActive_ = false; } @@ -93,7 +96,7 @@ public: void set_emergencystop() override { - send_traction_message(TractionDefs::estop_set_payload()); + send_traction_message_with_loopback(TractionDefs::estop_set_payload()); estopActive_ = true; lastSetSpeed_.set_mph(0); } @@ -107,7 +110,8 @@ public: void set_fn(uint32_t address, uint16_t value) override { - send_traction_message(TractionDefs::fn_set_payload(address, value)); + send_traction_message_with_loopback( + TractionDefs::fn_set_payload(address, value)); lastKnownFn_[address] = value; } @@ -283,17 +287,57 @@ private: { if (listenConsist_) { - handler_.wait_for_response( - NodeHandle(dst_), TractionDefs::RESP_CONSIST_CONFIG, &timer_); - send_traction_message( - TractionDefs::consist_del_payload(node_->node_id())); - return sleep_and_call( - &timer_, TIMEOUT_NSEC, STATE(release_listener_response)); - } - else - { - return call_immediately(STATE(release_step_2)); + // Checks if there is another throttle listening to the same + // train. If so, we will not unregister ourselves. The last local + // throttle to release will do the unregistering. + bool found_other_throttle = false; + { + AtomicHolder h(LinkedObject::head_mu()); + for (TractionThrottle *p = + LinkedObject::link_head(); + p && !found_other_throttle; + p = p->LinkedObject::link_next()) + { + if (p == this) + { + // self, ignore + continue; + } + if (p->node_ != node_) + { + // Different virtual node, will get regular + // feedback + continue; + } + if (p->dst_ != dst_) + { + // Target node ID is different. + continue; + } + if (!p->listenConsist_) + { + // other throttle was not set as listener to begin with + continue; + } + found_other_throttle = true; + } + } + if (!found_other_throttle) + { + LOG(VERBOSE, "unregister listener"); + handler_.wait_for_response(NodeHandle(dst_), + TractionDefs::RESP_CONSIST_CONFIG, &timer_); + send_traction_message( + TractionDefs::consist_del_payload(node_->node_id())); + return sleep_and_call( + &timer_, TIMEOUT_NSEC, STATE(release_listener_response)); + } + LOG(VERBOSE, + "skipping unregister consist because of another throttle."); + // we do have to remove the handler though. + clear_listening(); } + return call_immediately(STATE(release_step_2)); } Action release_listener_response() @@ -429,6 +473,8 @@ private: return false; } + /// Invoked for TRACTION_CONTROL_REPLY messages coming in via the + /// dispatcher. void speed_reply(Buffer *msg) { AutoReleaseBuffer rb(msg); @@ -594,6 +640,9 @@ private: return return_ok(); } + /// Invoked for TRACTION_CONTROL_COMMAND messages coming in via the + /// dispatcher. These are generally update commands coming on when another + /// throttle is controlling the same loco or consist via another member. void listen_reply(Buffer *msg) { AutoReleaseBuffer rb(msg); @@ -606,7 +655,15 @@ private: { return; } - const Payload &p = msg->data()->payload; + listen_reply_process(msg->data()->payload); + } + + /// Business logic for interpreting a proxied traction command payload. The + /// command may be coming back as a consist forward message due to throttle + /// listener, or may be one that went out from another TractionThrottle + /// instance to the same train. + void listen_reply_process(const Payload &p) + { if (p.size() < 1) return; switch (p[0] & TractionDefs::REQ_MASK) @@ -653,16 +710,109 @@ private: } } - /** Allocates (synchronously) an outgoing openlcb buffer with traction - * request MTI and the given payload and sends off the message to the bus - * for dst_. */ - void send_traction_message(const Payload &payload) + /// Allocates (synchronously) an outgoing openlcb buffer with traction + /// request MTI and the given payload and sends off the message to the bus + /// for dst_. + /// + /// Performs loopback to other traction throttles that might be assigned to + /// the same train. + /// + /// @param payload is the data contents of the message + /// (e.g. TractionDefs::speed_set_payload(...). + void send_traction_message_with_loopback(Payload payload) + { + auto b = send_traction_message_helper(std::move(payload)); + std::function f = std::bind( + &TractionThrottle::loopback_traction_message, this, b.release()); + iface()->executor()->add(new CallbackExecutable(std::move(f))); + } + + /// Performs loopback processing of an outgoing traction message. Run on + /// the iface()'s executor. + /// + /// @param b the message that was sent out. Will be unreffed. + void loopback_traction_message(Buffer* b) + { + auto rb = get_buffer_deleter(b); + // Walks all TractionThrottle objects. + TractionThrottle *p = nullptr; + do + { + { + // finds next instance that's interesting + AtomicHolder h(LinkedObject::head_mu()); + while (true) + { + if (!p) + { + p = LinkedObject::link_head(); + } + else + { + p = p->LinkedObject::link_next(); + } + if (!p) + { + break; + } + if (p == this) + { + // self, ignore + continue; + } + if (p->node_ != node_) + { + // Differnet virtual node, will get regular + // feedback + continue; + } + if (p->dst_ != dst_) + { + // Target node ID is different. + continue; + } + if (!p->listenConsist_) + { + // other throttle was not set as listener to begin with + continue; + } + + // Will call p, but we need to get out of the + // atomic first. + break; + } + } // atomic + if (p) + { + p->listen_reply_process(b->data()->payload); + } + } while (p != nullptr); + } + + /// Allocates (synchronously) an outgoing openlcb buffer with traction + /// request MTI and the given payload and sends off the message to the bus + /// for dst_. + /// + /// @param payload is the data contents of the message + /// (e.g. TractionDefs::speed_set_payload(...). + void send_traction_message(Payload payload) + { + send_traction_message_helper(std::move(payload)); + } + + /// Allocates (synchronously) an outgoing openlcb buffer with traction + /// request MTI and the given payload and sends off the message to the bus + /// for dst_. + /// + /// Returns a reference to the buffer. + BufferPtr send_traction_message_helper(Payload payload) { HASSERT(dst_ != 0); auto *b = iface()->addressed_message_write_flow()->alloc(); b->data()->reset(Defs::MTI_TRACTION_CONTROL_COMMAND, node_->node_id(), - NodeHandle(dst_), payload); - iface()->addressed_message_write_flow()->send(b); + NodeHandle(dst_), std::move(payload)); + iface()->addressed_message_write_flow()->send(b->ref()); + return get_buffer_deleter(b); } void set_listening() From b53552b360392a8879cccd49798a448a68d930b3 Mon Sep 17 00:00:00 2001 From: Balazs Racz Date: Sun, 28 Jan 2024 21:41:27 +0100 Subject: [PATCH 04/16] Adds support for Offset(n) attribute in the CDI (#765) Adds support for Offset(n) attribute in the CDI for individual entries, including groups, atoms (like string) or number entries. It is also supported to declare a negative offset, which is a typical use- case when the entries are displayed out of order, or when one source data needs to be displayed in two ways. Previous Offset() was only supported on segments, which are a form of groups. With this PR, the declaration is moved up to the root class of all config entries. From then on every entry must have some form of definition. Also adjusts how the c++ constexpr generates the offset() attribute to take this into account. --- src/openlcb/ConfigRenderer.hxx | 36 ++++++++++++++++++++++++---- src/openlcb/ConfigRepresentation.hxx | 3 ++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/openlcb/ConfigRenderer.hxx b/src/openlcb/ConfigRenderer.hxx index a401d1365..c32e55783 100644 --- a/src/openlcb/ConfigRenderer.hxx +++ b/src/openlcb/ConfigRenderer.hxx @@ -37,6 +37,7 @@ #include #include +#include #include "openlcb/SimpleNodeInfoDefs.hxx" #include "utils/OptionalArgs.hxx" @@ -52,7 +53,8 @@ struct AtomConfigDefs DECLARE_OPTIONALARG(Description, description, const char *, 1, nullptr); DECLARE_OPTIONALARG(MapValues, mapvalues, const char *, 2, nullptr); DECLARE_OPTIONALARG(SkipInit, skip_init, int, 15, 0); - using Base = OptionalArg; + DECLARE_OPTIONALARG(Offset, offset, int, 10, 0); + using Base = OptionalArg; }; /// Configuration implementation class for CDI Atom elements (strings, events @@ -72,6 +74,10 @@ public: /// When set to true, the event initializers will be skipped in this event /// or group. DEFINE_OPTIONALARG(SkipInit, skip_init, int); + /// Represents the 'offset' attribute for groups and elements and the + /// 'origin' attribute for segments. + DEFINE_OPTIONALARG(Offset, offset, int); + void render_cdi(std::string *r) const { @@ -115,6 +121,11 @@ public: { *s += StringPrintf(" size=\'%u\'", size_); } + int ofs = AtomConfigOptions(args...).offset(); + if (ofs != 0) + { + *s += StringPrintf(" offset=\'%d\'", ofs); + } *s += ">\n"; AtomConfigOptions(args...).render_cdi(s); *s += StringPrintf("\n", tag_); @@ -136,7 +147,7 @@ struct NumericConfigDefs : public AtomConfigDefs DECLARE_OPTIONALARG(Max, maxvalue, int, 7, INT_MAX); DECLARE_OPTIONALARG(Default, defaultvalue, int, 8, INT_MAX); using Base = OptionalArg; + Min, Max, Default, SkipInit, Offset>; }; /// Definitions for the options for numeric CDI entries. @@ -157,6 +168,7 @@ public: DEFINE_OPTIONALARG(Max, maxvalue, int); DEFINE_OPTIONALARG(Default, defaultvalue, int); DEFINE_OPTIONALARG(SkipInit, skip_init, int); + DEFINE_OPTIONALARG(Offset, offset, int); void render_cdi(std::string *r) const { @@ -222,6 +234,11 @@ public: { *s += StringPrintf(" size=\'%u\'", size_); } + int ofs = NumericConfigOptions(args...).offset(); + if (ofs != 0) + { + *s += StringPrintf(" offset=\'%d\'", ofs); + } *s += ">\n"; NumericConfigOptions(args...).render_cdi(s); *s += StringPrintf("\n", tag_); @@ -241,13 +258,12 @@ struct GroupConfigDefs : public AtomConfigDefs { // This is needed for inheriting declarations. using AtomConfigDefs::check_arguments_are_valid; - DECLARE_OPTIONALARG(Offset, offset, int, 10, INT_MAX); DECLARE_OPTIONALARG(Segment, segment, int, 11, -1); DECLARE_OPTIONALARG(RepName, repname, const char*, 12, nullptr); DECLARE_OPTIONALARG(FixedSize, fixed_size, unsigned, 13, 0); DECLARE_OPTIONALARG(Hidden, hidden, int, 14, 0); using Base = OptionalArg