From ea668f18c1c2c1107463880c95e6d64c2bfc9057 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Mon, 19 Jan 2026 18:22:50 -0600 Subject: [PATCH 01/16] Initial Tlmpacketizer Changes --- .../CdhCoreConfig/CdhCoreTlmConfig.fpp | 38 +++---- Svc/TlmPacketizer/TlmPacketizer.cpp | 105 ++++++++++++++++-- Svc/TlmPacketizer/TlmPacketizer.fpp | 31 +++++- Svc/TlmPacketizer/TlmPacketizer.hpp | 31 +++++- .../test/ut/TlmPacketizerTester.cpp | 11 +- default/config/CMakeLists.txt | 1 + default/config/TlmPacketizerCfg.fpp | 8 ++ 7 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 default/config/TlmPacketizerCfg.fpp diff --git a/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp b/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp index bb69471cdb4..52adeb96f6a 100644 --- a/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp +++ b/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp @@ -1,24 +1,24 @@ module CdhCore{ - instance tlmSend: Svc.TlmChan base id CdhCoreConfig.BASE_ID + 0x06000 \ - queue size CdhCoreConfig.QueueSizes.tlmSend \ - stack size CdhCoreConfig.StackSizes.tlmSend \ - priority CdhCoreConfig.Priorities.tlmSend \ + # instance tlmSend: Svc.TlmChan base id CdhCoreConfig.BASE_ID + 0x06000 \ + # queue size CdhCoreConfig.QueueSizes.tlmSend \ + # stack size CdhCoreConfig.StackSizes.tlmSend \ + # priority CdhCoreConfig.Priorities.tlmSend \ # Uncomment the following block and comment the above block to use TlmPacketizer instead of TlmChan - #instance tlmSend: Svc.TlmPacketizer base id CdhCoreConfig.BASE_ID + 0x06000 \ - # queue size CdhCoreConfig.QueueSizes.tlmSend \ - # stack size CdhCoreConfig.StackSizes.tlmSend \ - # priority CdhCoreConfig.Priorities.tlmSend \ - #{ - # # NOTE: The Name Ref is specific to the Reference deployment, Ref - # # This name will need to be updated if wishing to use this in a custom deployment - # phase Fpp.ToCpp.Phases.configComponents """ - # CdhCore::tlmSend.setPacketList( - # Ref::Ref_RefPacketsTlmPackets::packetList, - # Ref::Ref_RefPacketsTlmPackets::omittedChannels, - # 1 - # ); - # """ - #} + instance tlmSend: Svc.TlmPacketizer base id CdhCoreConfig.BASE_ID + 0x06000 \ + queue size CdhCoreConfig.QueueSizes.tlmSend \ + stack size CdhCoreConfig.StackSizes.tlmSend \ + priority CdhCoreConfig.Priorities.tlmSend \ + { + # NOTE: The Name Ref is specific to the Reference deployment, Ref + # This name will need to be updated if wishing to use this in a custom deployment + phase Fpp.ToCpp.Phases.configComponents """ + CdhCore::tlmSend.setPacketList( + TestDeployment::TestDeployment_TestDeploymentPacketsTlmPackets::packetList, + TestDeployment::TestDeployment_TestDeploymentPacketsTlmPackets::omittedChannels, + 1 + ); + """ + } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 484cf3ca56e..cb545008b2d 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -358,16 +358,66 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // push all updated packet buffers for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { + // If packet received updates, set an update flag for each output port + + // FwChanIdType packetLevel = this->m_sendBuffers->level; if (this->m_sendBuffers[pkt].updated) { - // serialize time into time offset in packet - Fw::ExternalSerializeBuffer buff( - &this->m_sendBuffers[pkt] - .buffer.getBuffAddr()[sizeof(FwPacketDescriptorType) + sizeof(FwTlmPacketizeIdType)], - Fw::Time::SERIALIZED_SIZE); - Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); - FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); - - this->PktSend_out(0, this->m_sendBuffers[pkt].buffer, 0); + for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) + { + this->m_sendUpdateFlag[port][pkt] = true; + } + this->m_sendBuffers[pkt].updated = false; + } + for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) + { + // Fw::Enabled port_enable = this-> + + + + if (not this->isConnected_PktSend_OutputPort(port)) + { + continue; + } + + // If packet has been updated since last sent on port + if (this->m_sendUpdateFlag[port][pkt]) { + // If delta min is disabled (Disable on change, Only send on Delta Max) + if (this->m_deltaConfig[port][pkt].min == 0xFFFFFFFF) { + this->m_sendUpdateFlag[port][pkt] = false; + + // If counter is less than when it should be sent. + } else if (this->m_sendCounter[port][pkt] < this->m_deltaConfig[port][pkt].min) { + // Keep flag true but do not send. + continue; + } + } + + // If Delta Max is configured and the send counter for this port and packet is greater than Delta Max + if (this->m_deltaConfig[port][pkt].max != 0xFFFFFFFF and this->m_sendCounter[port][pkt] >= this->m_deltaConfig[port][pkt].max) + { + // Signal an update if counter exceeded max counts for current port + this->m_sendUpdateFlag[port][pkt] = true; + } + + // Send under the following conditions: + // 1. Packet received updates and it has been past delta min counts since last packet + // 2. Packet has passed delta max counts since last packet + if (this->m_sendUpdateFlag[port][pkt]) { + // serialize time into time offset in packet + Fw::ExternalSerializeBuffer buff( + &this->m_sendBuffers[pkt] + .buffer.getBuffAddr()[sizeof(FwPacketDescriptorType) + sizeof(FwTlmPacketizeIdType)], + Fw::Time::SERIALIZED_SIZE); + Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); + FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); + + this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); + this->m_sendUpdateFlag[port][pkt] = false; + this->m_sendCounter[port][pkt] = 0; + } else { + this->m_sendCounter[port][pkt]++; + } + } } } @@ -416,6 +466,43 @@ void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cm this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } +void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwIndexType portOut, + FwChanIdType tlmGroup, + Fw::Enabled enable) { + // lol + + if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + } + if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + } + this->m_groupEnabled[portOut][tlmGroup] = enable; + + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwIndexType portOut, + FwChanIdType tlmGroup, + U32 minDelta, + U32 maxDelta) { + if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + } + if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + } + if (minDelta > maxDelta ) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + } + this->m_deltaConfig[portOut][tlmGroup] = {minDelta, maxDelta}; + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + FwChanIdType TlmPacketizer::doHash(FwChanIdType id) { return (id % TLMPACKETIZER_HASH_MOD_VALUE) % TLMPACKETIZER_NUM_TLM_HASH_SLOTS; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index f03dbafffa1..57f950bbcad 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -8,7 +8,7 @@ module Svc { # ---------------------------------------------------------------------- @ Packet send port - output port PktSend: Fw.Com + output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS] Fw.Com @ Ping input port async input port pingIn: Svc.Ping @@ -50,6 +50,12 @@ module Svc { @ Time get time get port timeGetOut + @ Param Get + param get port prmGetOut + + @ Param Set + param set port prmSetOut + # ---------------------------------------------------------------------- # Commands # ---------------------------------------------------------------------- @@ -66,6 +72,23 @@ module Svc { ) \ opcode 1 + async command ENABLE_GROUP( + portOut: FwIndexType @< Port to configure + tlmGroup: FwChanIdType @< Group Level + enable: Fw.Enabled @< Active Sending Group + ) \ + opcode 2 + + async command SET_GROUP_DELTAS( + portOut: FwIndexType @< Port to configure + tlmGroup: FwChanIdType @< Group Level + minDelta: U32 + maxDelta: U32 + ) \ + opcode 3 + + + # ---------------------------------------------------------------------- # Events # ---------------------------------------------------------------------- @@ -118,6 +141,12 @@ module Svc { @ Telemetry send level telemetry SendLevel: FwChanIdType id 0 + # ---------------------------------------------------------------------- + # Params + # ---------------------------------------------------------------------- + + array ForceConfigurations = [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS] Fw.Active default Fw.Active.ACTIVE + param FORCE_OUT: ForceConfigurations } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 08948d50558..06143bc20b2 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -15,6 +15,7 @@ #include "Svc/TlmPacketizer/TlmPacketizerComponentAc.hpp" #include "Svc/TlmPacketizer/TlmPacketizerTypes.hpp" #include "config/TlmPacketizerCfg.hpp" +#include "Fw/Types/EnabledEnumAc.hpp" namespace Svc { @@ -85,6 +86,22 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { U32 id /*!< The packet ID*/ ) override; + //! Handler implementation for command ENABLE_GROUP + void ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType portOut, //!< Port to configure + FwChanIdType tlmGroup, //!< Group Level + Fw::Enabled enable //!< Active Sending Group + ) override; + + //! Handler implementation for command SET_GROUP_DELTAS + void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType portOut, //!< Port to configure + FwChanIdType tlmGroup, //!< Group Level + U32 minDelta, + U32 maxDelta) override; + // number of packets to fill FwChanIdType m_numPackets; // Array of packet buffers to send @@ -126,7 +143,8 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { // hash function for looking up telemetry channel FwChanIdType doHash(FwChanIdType id); - Os::Mutex m_lock; //!< used to lock access to packet buffers + Os::Mutex m_lock; //!< used to lock access to packet buffers + Os::Mutex m_lock_param; //!< used to lock access to parameters bool m_configured; //!< indicates a table has been passed and packets configured @@ -141,6 +159,17 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType m_startLevel; //!< initial level for sending packets FwChanIdType m_maxLevel; //!< maximum level in all packets + + U32 m_sendCounter[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS] {0}; //!< Counter for Primary/Secondary/Tertiary RG driven send deltas + + struct deltaConfig { + U32 min = 0; //!< Default for Backwards Compatible Behavior + U32 max = 0xFFFFFFFF; //!< Default for Backwards Compatible Behavior + } m_deltaConfig[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{}; + + Fw::Enabled m_groupEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{Fw::Enabled::ENABLED}; + bool m_sendUpdateFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{false}; + }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 6e387af7d50..ba1de3d3609 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -26,6 +26,7 @@ TlmPacketizerTester ::TlmPacketizerTester() : TlmPacketizerGTestBase("Tester", MAX_HISTORY_SIZE), component("TlmPacketizer") { this->initComponents(); this->connectPorts(); + this->component.loadParameters(); } TlmPacketizerTester ::~TlmPacketizerTester() { @@ -945,7 +946,9 @@ void TlmPacketizerTester ::getChannelValueTest() { // ---------------------------------------------------------------------- void TlmPacketizerTester ::from_PktSend_handler(const FwIndexType portNum, Fw::ComBuffer& data, U32 context) { - this->pushFromPortEntry_PktSend(data, context); + if (portNum == 0) { + this->pushFromPortEntry_PktSend(data, context); + } } void TlmPacketizerTester ::from_pingOut_handler(const FwIndexType portNum, U32 key) { @@ -959,6 +962,8 @@ void TlmPacketizerTester ::from_pingOut_handler(const FwIndexType portNum, U32 k void TlmPacketizerTester ::connectPorts() { // PktSend this->component.set_PktSend_OutputPort(0, this->get_from_PktSend(0)); + this->component.set_PktSend_OutputPort(1, this->get_from_PktSend(1)); + this->component.set_PktSend_OutputPort(2, this->get_from_PktSend(2)); // Run this->connect_to_Run(0, this->component.get_Run_InputPort(0)); @@ -995,6 +1000,10 @@ void TlmPacketizerTester ::connectPorts() { // TlmGet this->connect_to_TlmGet(0, this->component.get_TlmGet_InputPort(0)); + + // Params + this->component.set_prmGetOut_OutputPort(0, this->get_from_prmGetOut(0)); + this->component.set_prmSetOut_OutputPort(0, this->get_from_prmSetOut(0)); } void TlmPacketizerTester::textLogIn(const FwEventIdType id, //!< The event ID diff --git a/default/config/CMakeLists.txt b/default/config/CMakeLists.txt index 935d998034e..a3a2bfcadcb 100644 --- a/default/config/CMakeLists.txt +++ b/default/config/CMakeLists.txt @@ -18,6 +18,7 @@ register_fprime_config( "${CMAKE_CURRENT_LIST_DIR}/PlatformCfg.fpp" "${CMAKE_CURRENT_LIST_DIR}/PolyDbCfg.fpp" "${CMAKE_CURRENT_LIST_DIR}/VersionCfg.fpp" + "${CMAKE_CURRENT_LIST_DIR}/TlmPacketizerCfg.fpp" HEADERS "${CMAKE_CURRENT_LIST_DIR}/EventManagerCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/ActiveRateGroupCfg.hpp" diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp new file mode 100644 index 00000000000..5076132b11f --- /dev/null +++ b/default/config/TlmPacketizerCfg.fpp @@ -0,0 +1,8 @@ +# ====================================================================== +# TlmPacketizerCfg.fpp +# Constants for configuring TlmPacketizer Send Logic Levels +# ====================================================================== +module Svc { + constant NUM_CONFIGURABLE_TLMPACKETIZER_PORTS = 3; + constant NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS = 1; +} From e4b61e2c29fe6deca4737c0b8fd540331fb780e2 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Tue, 20 Jan 2026 14:03:35 -0600 Subject: [PATCH 02/16] Configure Tlm Groups --- Svc/TlmPacketizer/TlmPacketizer.cpp | 71 ++++++++++++++++------------- Svc/TlmPacketizer/TlmPacketizer.hpp | 8 ++-- default/config/TlmPacketizerCfg.fpp | 2 +- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index cb545008b2d..6e87533dbfd 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -353,56 +353,53 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } else { this->m_sendBuffers[pkt].updated = false; } + + // Update per port group flags + if (this->m_sendBuffers[pkt].updated == true) { + for (FwIndexType port = 0; port < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; port++) { + this->m_sendUpdateFlag[port][this->m_sendBuffers[pkt].level] = true; + } + this->m_sendBuffers[pkt].updated = false; + } } this->m_lock.unLock(); // push all updated packet buffers for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { - // If packet received updates, set an update flag for each output port - - // FwChanIdType packetLevel = this->m_sendBuffers->level; - if (this->m_sendBuffers[pkt].updated) { - for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) - { - this->m_sendUpdateFlag[port][pkt] = true; - } - this->m_sendBuffers[pkt].updated = false; - } + FwChanIdType entryLevel = this->m_sendBuffers[pkt].level; + + // Iterate through output ports for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { - // Fw::Enabled port_enable = this-> - - - if (not this->isConnected_PktSend_OutputPort(port)) { continue; } - // If packet has been updated since last sent on port - if (this->m_sendUpdateFlag[port][pkt]) { + // If a packet in the group has been updated since last sent on port + if (this->m_sendUpdateFlag[port][entryLevel]) { // If delta min is disabled (Disable on change, Only send on Delta Max) - if (this->m_deltaConfig[port][pkt].min == 0xFFFFFFFF) { - this->m_sendUpdateFlag[port][pkt] = false; + if (this->m_deltaConfig[port][entryLevel].min == 0xFFFFFFFF) { + this->m_sendUpdateFlag[port][entryLevel] = false; - // If counter is less than when it should be sent. - } else if (this->m_sendCounter[port][pkt] < this->m_deltaConfig[port][pkt].min) { + // If counter is less than delta min + } else if (this->m_sendCounter[port][entryLevel] < this->m_deltaConfig[port][entryLevel].min) { // Keep flag true but do not send. continue; } } // If Delta Max is configured and the send counter for this port and packet is greater than Delta Max - if (this->m_deltaConfig[port][pkt].max != 0xFFFFFFFF and this->m_sendCounter[port][pkt] >= this->m_deltaConfig[port][pkt].max) + if (this->m_deltaConfig[port][entryLevel].max != 0xFFFFFFFF and this->m_sendCounter[port][entryLevel] >= this->m_deltaConfig[port][entryLevel].max) { // Signal an update if counter exceeded max counts for current port - this->m_sendUpdateFlag[port][pkt] = true; + this->m_sendUpdateFlag[port][entryLevel] = true; } // Send under the following conditions: - // 1. Packet received updates and it has been past delta min counts since last packet - // 2. Packet has passed delta max counts since last packet - if (this->m_sendUpdateFlag[port][pkt]) { + // 1. Packet received updates and it has been past delta min counts since last packet (and min is configured) + // 2. Packet has passed delta max counts since last packet (and max is configured) + if (this->m_sendUpdateFlag[port][entryLevel]) { // serialize time into time offset in packet Fw::ExternalSerializeBuffer buff( &this->m_sendBuffers[pkt] @@ -412,12 +409,22 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); - this->m_sendUpdateFlag[port][pkt] = false; - this->m_sendCounter[port][pkt] = 0; - } else { - this->m_sendCounter[port][pkt]++; + this->m_sentFlag[port][entryLevel] = true; + } + } + } + + // Update Timing Flags based on Sent Behavior + for (FwIndexType entryPort = 0; entryPort < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; entryPort++) + { + for (FwChanIdType entryGroup = 0; entryGroup < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; entryGroup++) + { + this->m_sendCounter[entryPort][entryGroup]++; + // If the group has been sent recently, reset values + if (this->m_sentFlag[entryPort][entryGroup]) { + this->m_sendCounter[entryPort][entryGroup] = 0; + this->m_sendUpdateFlag[entryPort][entryGroup] = false; } - } } } @@ -476,7 +483,7 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS) { + if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } this->m_groupEnabled[portOut][tlmGroup] = enable; @@ -493,7 +500,7 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS) { + if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } if (minDelta > maxDelta ) { diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 06143bc20b2..3a9e5945c17 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -160,16 +160,16 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType m_startLevel; //!< initial level for sending packets FwChanIdType m_maxLevel; //!< maximum level in all packets - U32 m_sendCounter[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS] {0}; //!< Counter for Primary/Secondary/Tertiary RG driven send deltas + U32 m_sendCounter[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] {0}; //!< Counter for Primary/Secondary/Tertiary RG driven send deltas struct deltaConfig { U32 min = 0; //!< Default for Backwards Compatible Behavior U32 max = 0xFFFFFFFF; //!< Default for Backwards Compatible Behavior } m_deltaConfig[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{}; - - Fw::Enabled m_groupEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{Fw::Enabled::ENABLED}; - bool m_sendUpdateFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{false}; + Fw::Enabled m_groupEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{Fw::Enabled::ENABLED}; + bool m_sendUpdateFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{false}; + bool m_sentFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{false}; }; } // end namespace Svc diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp index 5076132b11f..82b69e9d332 100644 --- a/default/config/TlmPacketizerCfg.fpp +++ b/default/config/TlmPacketizerCfg.fpp @@ -4,5 +4,5 @@ # ====================================================================== module Svc { constant NUM_CONFIGURABLE_TLMPACKETIZER_PORTS = 3; - constant NUM_CONFIGURABLE_TLMPACKETIZER_LEVELS = 1; + constant NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS = 10; } From bf5053c749d612f6b8f52f31b942eae3a2706b1d Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Thu, 22 Jan 2026 15:57:47 -0600 Subject: [PATCH 03/16] Impl UT --- Svc/TlmPacketizer/TlmPacketizer.cpp | 112 ++++++----- Svc/TlmPacketizer/TlmPacketizer.fpp | 20 +- Svc/TlmPacketizer/TlmPacketizer.hpp | 44 +++-- .../test/ut/TlmPacketizerTester.cpp | 187 +++++++++++++++++- .../test/ut/TlmPacketizerTester.hpp | 6 + Svc/TlmPacketizer/test/ut/main.cpp | 5 + default/config/TlmPacketizerCfg.fpp | 2 +- 7 files changed, 306 insertions(+), 70 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 6e87533dbfd..5aaa0aa14ed 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -357,7 +357,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Update per port group flags if (this->m_sendBuffers[pkt].updated == true) { for (FwIndexType port = 0; port < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; port++) { - this->m_sendUpdateFlag[port][this->m_sendBuffers[pkt].level] = true; + this->m_packetFlags[port][pkt].updateFlag = true; } this->m_sendBuffers[pkt].updated = false; } @@ -366,40 +366,62 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // push all updated packet buffers for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { - FwChanIdType entryLevel = this->m_sendBuffers[pkt].level; + FwChanIdType entryGroup = this->m_sendBuffers[pkt].level; // Iterate through output ports for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { + PktSendCounters& pktEntryFlags = this->m_packetFlags[port][pkt]; + pktEntryFlags.prevSentCounter++; + + if (not this->isConnected_PktSend_OutputPort(port)) { continue; } - // If a packet in the group has been updated since last sent on port - if (this->m_sendUpdateFlag[port][entryLevel]) { - // If delta min is disabled (Disable on change, Only send on Delta Max) - if (this->m_deltaConfig[port][entryLevel].min == 0xFFFFFFFF) { - this->m_sendUpdateFlag[port][entryLevel] = false; - - // If counter is less than delta min - } else if (this->m_sendCounter[port][entryLevel] < this->m_deltaConfig[port][entryLevel].min) { - // Keep flag true but do not send. + if (entryGroup <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP and not this->m_sendBuffers[pkt].requested) { + GroupConfig& entryGroupConfig = this->m_groupConfigs[port][entryGroup]; + if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) + { continue; } - } - // If Delta Max is configured and the send counter for this port and packet is greater than Delta Max - if (this->m_deltaConfig[port][entryLevel].max != 0xFFFFFFFF and this->m_sendCounter[port][entryLevel] >= this->m_deltaConfig[port][entryLevel].max) - { - // Signal an update if counter exceeded max counts for current port - this->m_sendUpdateFlag[port][entryLevel] = true; + // If a packet in the group has been updated since last sent on port + if (pktEntryFlags.updateFlag) { + // If delta min is disabled (Disable on change, Only send on Delta Max) + if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::EVERY_MAX) { + pktEntryFlags.updateFlag = false; + + // If counter is less than delta min + } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { + // Keep flag true but do not send. + pktEntryFlags.prevSentCounter++; + continue; + } + } + + // If Delta Max is configured and the send counter for this port and packet is greater than Delta Max + if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and + pktEntryFlags.prevSentCounter >= entryGroupConfig.max) + { + // Signal an update if counter exceeded max counts for current port + pktEntryFlags.updateFlag = true; + } + + if (not (entryGroupConfig.enabled or entryGroupConfig.forceEnabled)) { + pktEntryFlags.updateFlag = false; + } } - + // Send under the following conditions: // 1. Packet received updates and it has been past delta min counts since last packet (and min is configured) // 2. Packet has passed delta max counts since last packet (and max is configured) - if (this->m_sendUpdateFlag[port][entryLevel]) { + // With the above, the group must be either enabled or force enabled. + // 3. If the group of the packet lies above the max configurable tlmpacketizer group, update behavior is + // determined fully on updates or emitted always. + // 4. If the packet was requested. + if (pktEntryFlags.updateFlag) { // serialize time into time offset in packet Fw::ExternalSerializeBuffer buff( &this->m_sendBuffers[pkt] @@ -409,23 +431,11 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); - this->m_sentFlag[port][entryLevel] = true; - } - } - } - - // Update Timing Flags based on Sent Behavior - for (FwIndexType entryPort = 0; entryPort < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; entryPort++) - { - for (FwChanIdType entryGroup = 0; entryGroup < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; entryGroup++) - { - this->m_sendCounter[entryPort][entryGroup]++; - // If the group has been sent recently, reset values - if (this->m_sentFlag[entryPort][entryGroup]) { - this->m_sendCounter[entryPort][entryGroup] = 0; - this->m_sendUpdateFlag[entryPort][entryGroup] = false; + pktEntryFlags.prevSentCounter = 0; + pktEntryFlags.updateFlag = false; } } + this->m_sendBuffers[pkt].requested = false; } } @@ -478,16 +488,22 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, FwIndexType portOut, FwChanIdType tlmGroup, Fw::Enabled enable) { - // lol - - if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { + if (portOut > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS) { + this->m_groupConfigs[portOut][tlmGroup].enabled = enable; + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwIndexType portOut, + FwChanIdType tlmGroup, + Fw::Enabled enable) { + if (portOut > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - this->m_groupEnabled[portOut][tlmGroup] = enable; - + this->m_groupConfigs[portOut][tlmGroup].forceEnabled = enable; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -495,18 +511,18 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwIndexType portOut, FwChanIdType tlmGroup, + Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (portOut > NUM_PKTSEND_OUTPUT_PORTS) { - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); - } - if (tlmGroup > NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS) { - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); - } - if (minDelta > maxDelta ) { + if (portOut > NUM_PKTSEND_OUTPUT_PORTS or + tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP or + minDelta > maxDelta) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - this->m_deltaConfig[portOut][tlmGroup] = {minDelta, maxDelta}; + GroupConfig& groupConfig = this->m_groupConfigs[portOut][tlmGroup]; + groupConfig.rateLogic = rateLogic; + groupConfig.min = minDelta; + groupConfig.max = maxDelta; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 57f950bbcad..eb730d87618 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -1,8 +1,12 @@ module Svc { - @ A component for storing telemetry active component TlmPacketizer { - + enum RateLogic { + SILENCED, + EVERY_MAX, + ON_CHANGE_MIN, + ON_CHANGE_MIN_AND_EVERY_MAX, + } # ---------------------------------------------------------------------- # General ports # ---------------------------------------------------------------------- @@ -75,17 +79,25 @@ module Svc { async command ENABLE_GROUP( portOut: FwIndexType @< Port to configure tlmGroup: FwChanIdType @< Group Level - enable: Fw.Enabled @< Active Sending Group + enable: Fw.Enabled @< Active Sending Group ) \ opcode 2 + + async command FORCE_GROUP( + portOut: FwIndexType @< Port to configure + tlmGroup: FwChanIdType @< Group Level + enable: Fw.Enabled @< Active Sending Group + ) \ + opcode 3 async command SET_GROUP_DELTAS( portOut: FwIndexType @< Port to configure tlmGroup: FwChanIdType @< Group Level + rateLogic: RateLogic @< Rate Logic minDelta: U32 maxDelta: U32 ) \ - opcode 3 + opcode 4 diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 3a9e5945c17..a78a749a8dc 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -20,6 +20,9 @@ namespace Svc { class TlmPacketizer final : public TlmPacketizerComponentBase { + + friend class TlmPacketizerTester; + public: // ---------------------------------------------------------------------- // Construction, initialization, and destruction @@ -94,11 +97,20 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { Fw::Enabled enable //!< Active Sending Group ) override; + //! Handler implementation for command FORCE_GROUP + void FORCE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType portOut, //!< Port to configure + FwChanIdType tlmGroup, //!< Group Level + Fw::Enabled enable //!< Active Sending Group + ) override; + //! Handler implementation for command SET_GROUP_DELTAS - void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq, //!< The command sequence number - FwIndexType portOut, //!< Port to configure - FwChanIdType tlmGroup, //!< Group Level + void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType portOut, //!< Port to configure + FwChanIdType tlmGroup, //!< Group Level + Svc::TlmPacketizer_RateLogic rateLogic, //!< Rate Logic U32 minDelta, U32 maxDelta) override; @@ -159,17 +171,19 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType m_startLevel; //!< initial level for sending packets FwChanIdType m_maxLevel; //!< maximum level in all packets - - U32 m_sendCounter[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] {0}; //!< Counter for Primary/Secondary/Tertiary RG driven send deltas - - struct deltaConfig { - U32 min = 0; //!< Default for Backwards Compatible Behavior - U32 max = 0xFFFFFFFF; //!< Default for Backwards Compatible Behavior - } m_deltaConfig[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{}; - - Fw::Enabled m_groupEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{Fw::Enabled::ENABLED}; - bool m_sendUpdateFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{false}; - bool m_sentFlag[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{false}; + + struct GroupConfig { + Fw::Enabled enabled = Fw::Enabled::ENABLED; + Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; + TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; + U32 min = 0; //!< Default for Backwards Compatible Behavior + U32 max = 0; //!< Default for Backwards Compatible Behavior + } m_groupConfigs[NUM_PKTSEND_OUTPUT_PORTS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; + + struct PktSendCounters { + U32 prevSentCounter = 0; + bool updateFlag = false; + } m_packetFlags[NUM_PKTSEND_OUTPUT_PORTS][MAX_PACKETIZER_PACKETS]{}; }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index ba1de3d3609..82fe91ded3a 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -43,11 +43,20 @@ TlmPacketizerChannelEntry packet1List[] = {{10, 4}, {100, 2}, {333, 1}}; TlmPacketizerChannelEntry packet2List[] = {{10, 4}, {13, 8}, {250, 2}, {22, 1}}; +TlmPacketizerChannelEntry packet3List[] = {{10, 4}, {67, 4}}; + +TlmPacketizerChannelEntry packet4List[] = {{10, 4}, {60, 4}}; + TlmPacketizerPacket packet1 = {packet1List, 4, 1, FW_NUM_ARRAY_ELEMENTS(packet1List)}; TlmPacketizerPacket packet2 = {packet2List, 8, 2, FW_NUM_ARRAY_ELEMENTS(packet2List)}; +TlmPacketizerPacket packet3 = {packet3List, 12, 1, FW_NUM_ARRAY_ELEMENTS(packet3List)}; + +TlmPacketizerPacket packet4 = {packet4List, 16, 2, FW_NUM_ARRAY_ELEMENTS(packet4List)}; + TlmPacketizerPacketList packetList = {{&packet1, &packet2}, 2}; +TlmPacketizerPacketList packetList2 = {{&packet1, &packet2, &packet3, &packet4}, 4}; TlmPacketizerChannelEntry ignoreList[] = {{25, 0}, {50, 0}}; @@ -941,14 +950,188 @@ void TlmPacketizerTester ::getChannelValueTest() { ASSERT_EQ(valid, Fw::TlmValid::INVALID); } +//! Configured tlm groups test +//! +void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { + this->component.setPacketList(packetList2, ignore, 4); + this->m_port0Lock = false; + Fw::Time time; + Fw::TlmBuffer buffer; + + // Arguments: opCode, cmdSeq, portOut, tlmGroup, minDelta, maxDelta + + // Group 1 + // Send Immediate On Change on port 0 + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 0, 0); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::ENABLED); + this->component.doDispatch(); + // Send every 5 on port 1 + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 5); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 1, 1, Fw::Enabled::ENABLED); + this->component.doDispatch(); + // Disable on Port 2 + this->sendCmd_ENABLE_GROUP(0, 0, 2, 1, Fw::Enabled::DISABLED); + this->component.doDispatch(); + this->clearHistory(); + + // Group 2 + // Send Every 2 + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 2); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 1, 2, Fw::Enabled::ENABLED); + this->component.doDispatch(); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 2, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 3, 5); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 2, 2, Fw::Enabled::ENABLED); + this->component.doDispatch(); + // Silence 2nd Packet on port 0 + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); + this->component.doDispatch(); + // Redundant Enable to Test silence logic. + this->sendCmd_ENABLE_GROUP(0, 0, 0, 2, Fw::Enabled::ENABLED); + this->component.doDispatch(); + this->clearHistory(); + + + + /* + Port 0 Gorup 1: 3, 15 MIN 3 + Port 1 Group 1: 2, 14 MIN 2 + Port 0 Group 2: 1, 4, 13, 16. MIN 1, MAX 12 + Port 1 Group 2: 0, 7, 12, 18. MIN 0, MAX 7 + Port 2 Group 1: 6, 18. MAX 6 + Port 2 group 2 Ignored + */ + + + // Music Melody Testing! + // Expected Behavior: + /* + + + T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 T=13 T=14 T=15 T=16 T=17 T=18 T=19 T=20 + + -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| + Port 0 Group 1 O O | | O O | | | + Port 1 Group 1 -O---------------O---------------|-------------------------------|-------------------------------O---------------O---------------|-------------------------------|-------------------------------| + Port 0 Group 2 O O O | O O O | | + Port 1 Group 2 -O-------------------------------|-----------------------O-------|-------------------------------O-------------------------------|-----------------------O-------|-------------------------------| + Port 2 Group 1 O | O | O | O | | + Port 2 Group 2 -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| + | | | | | | | + -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| + */ + + // Group 1 + // first channel + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(20))); + this->invoke_to_TlmRecv(0, 10, time, buffer); + + // second channel + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(15))); + this->invoke_to_TlmRecv(0, 100, time, buffer); + + // third channel + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(14))); + this->invoke_to_TlmRecv(0, 333, time, buffer); + + + // Group 2 + // 1st channel + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1000000))); + this->invoke_to_TlmRecv(0, 13, time, buffer); + + // 2nd channel + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1010))); + this->invoke_to_TlmRecv(0, 250, time, buffer); + + // 3rd channel + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(15))); + this->invoke_to_TlmRecv(0, 22, time, buffer); + + + this->setTestTime(this->m_testTime); + // run scheduler port to send packets + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(2); + ASSERT_from_PktSend_SIZE(2); + + // construct the packet buffers and make sure they are correct + + Fw::ComBuffer comBuff; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + + ASSERT_from_PktSend(1, comBuff, static_cast(0)); + + this->clearHistory(); + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(2); + ASSERT_from_PktSend_SIZE(2); + + // construct the packet buffers and make sure they are correct + + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + + ASSERT_from_PktSend(1, comBuff, static_cast(0)); +} + // ---------------------------------------------------------------------- // Handlers for typed from ports // ---------------------------------------------------------------------- void TlmPacketizerTester ::from_PktSend_handler(const FwIndexType portNum, Fw::ComBuffer& data, U32 context) { - if (portNum == 0) { - this->pushFromPortEntry_PktSend(data, context); + if (this->m_port0Lock && portNum != 0) { + return; } + this->pushFromPortEntry_PktSend(data, context); } void TlmPacketizerTester ::from_pingOut_handler(const FwIndexType portNum, U32 key) { diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp index 3467731bce4..08972313653 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp @@ -79,6 +79,10 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { //! void getChannelValueTest(void); + //! Configured tlm groups test + //! + void configuredTelemetryGroupsTests(void); + private: // ---------------------------------------------------------------------- // Handlers for typed from ports @@ -126,6 +130,8 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { TlmPacketizer component; Fw::Time m_testTime; //!< store test time for packets + + bool m_port0Lock{true}; //! Lock limited to entries from port 0 PktSend }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/main.cpp b/Svc/TlmPacketizer/test/ut/main.cpp index fba76bb7a21..c5b49248efb 100644 --- a/Svc/TlmPacketizer/test/ut/main.cpp +++ b/Svc/TlmPacketizer/test/ut/main.cpp @@ -74,6 +74,11 @@ TEST(TestNominal, TlmGetTest) { Svc::TlmPacketizerTester tester; tester.getChannelValueTest(); } +TEST(TestNominal, configuredTelemetryGroupsTests) { + TEST_CASE(100.1.9, "Configure Telem Send Levels and Rates"); + Svc::TlmPacketizerTester tester; + tester.configuredTelemetryGroupsTests(); +} int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp index 82b69e9d332..9faafc467be 100644 --- a/default/config/TlmPacketizerCfg.fpp +++ b/default/config/TlmPacketizerCfg.fpp @@ -4,5 +4,5 @@ # ====================================================================== module Svc { constant NUM_CONFIGURABLE_TLMPACKETIZER_PORTS = 3; - constant NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS = 10; + constant MAX_CONFIGURABLE_TLMPACKETIZER_GROUP = 3; } From a76515de23ead49f52a8fcde67e442cd00e6bb52 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Thu, 22 Jan 2026 20:08:13 -0600 Subject: [PATCH 04/16] UT impl --- Svc/TlmPacketizer/TlmPacketizer.cpp | 9 +- Svc/TlmPacketizer/TlmPacketizer.hpp | 2 +- .../test/ut/TlmPacketizerTester.cpp | 278 +++++++++++++----- 3 files changed, 213 insertions(+), 76 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 5aaa0aa14ed..b6e11173596 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -372,9 +372,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { PktSendCounters& pktEntryFlags = this->m_packetFlags[port][pkt]; - pktEntryFlags.prevSentCounter++; - - + // printf("TESTING PKT %d PORT %d\n", pkt, port); if (not this->isConnected_PktSend_OutputPort(port)) { continue; @@ -431,9 +429,14 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); + // printf("PKT SENT %d PORT %d\n", pkt, port); pktEntryFlags.prevSentCounter = 0; pktEntryFlags.updateFlag = false; } + if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) + { + pktEntryFlags.prevSentCounter++; + } } this->m_sendBuffers[pkt].requested = false; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index a78a749a8dc..6a7810f4d31 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -181,7 +181,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { } m_groupConfigs[NUM_PKTSEND_OUTPUT_PORTS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; struct PktSendCounters { - U32 prevSentCounter = 0; + U32 prevSentCounter = 0xFFFFFFFF; bool updateFlag = false; } m_packetFlags[NUM_PKTSEND_OUTPUT_PORTS][MAX_PACKETIZER_PACKETS]{}; }; diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 82fe91ded3a..abc63883c8e 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -43,7 +43,7 @@ TlmPacketizerChannelEntry packet1List[] = {{10, 4}, {100, 2}, {333, 1}}; TlmPacketizerChannelEntry packet2List[] = {{10, 4}, {13, 8}, {250, 2}, {22, 1}}; -TlmPacketizerChannelEntry packet3List[] = {{10, 4}, {67, 4}}; +TlmPacketizerChannelEntry packet3List[] = {{67, 4}}; TlmPacketizerChannelEntry packet4List[] = {{10, 4}, {60, 4}}; @@ -51,9 +51,9 @@ TlmPacketizerPacket packet1 = {packet1List, 4, 1, FW_NUM_ARRAY_ELEMENTS(packet1L TlmPacketizerPacket packet2 = {packet2List, 8, 2, FW_NUM_ARRAY_ELEMENTS(packet2List)}; -TlmPacketizerPacket packet3 = {packet3List, 12, 1, FW_NUM_ARRAY_ELEMENTS(packet3List)}; +TlmPacketizerPacket packet3 = {packet3List, 12, 2, FW_NUM_ARRAY_ELEMENTS(packet3List)}; -TlmPacketizerPacket packet4 = {packet4List, 16, 2, FW_NUM_ARRAY_ELEMENTS(packet4List)}; +TlmPacketizerPacket packet4 = {packet4List, 16, 3, FW_NUM_ARRAY_ELEMENTS(packet4List)}; TlmPacketizerPacketList packetList = {{&packet1, &packet2}, 2}; TlmPacketizerPacketList packetList2 = {{&packet1, &packet2, &packet3, &packet4}, 4}; @@ -956,171 +956,305 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->component.setPacketList(packetList2, ignore, 4); this->m_port0Lock = false; Fw::Time time; + // time = this->m_testTime; + // time.set(0, 0); + // this->m_testTime = time; + // this->setTestTime(this->m_testTime); + Fw::TlmBuffer buffer; + // this->invoke_to_Run(0, 0); + // this->component.doDispatch(); + // ASSERT_from_PktSend_SIZE(0); + + // time.set(100, 1000); + // Arguments: opCode, cmdSeq, portOut, tlmGroup, minDelta, maxDelta // Group 1 - // Send Immediate On Change on port 0 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 0, 0); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 3, 3); this->component.doDispatch(); this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::ENABLED); this->component.doDispatch(); + // Send every 5 on port 1 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 5); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 2, 2); this->component.doDispatch(); this->sendCmd_ENABLE_GROUP(0, 0, 1, 1, Fw::Enabled::ENABLED); this->component.doDispatch(); + // Disable on Port 2 this->sendCmd_ENABLE_GROUP(0, 0, 2, 1, Fw::Enabled::DISABLED); this->component.doDispatch(); this->clearHistory(); // Group 2 - // Send Every 2 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 2); - this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 1, 2, Fw::Enabled::ENABLED); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 4, 12); this->component.doDispatch(); - this->sendCmd_SET_GROUP_DELTAS(0, 0, 2, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 3, 5); + this->sendCmd_ENABLE_GROUP(0, 0, 0, 2, Fw::Enabled::ENABLED); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 2, 2, Fw::Enabled::ENABLED); + + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); this->component.doDispatch(); - // Silence 2nd Packet on port 0 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); + this->sendCmd_ENABLE_GROUP(0, 0, 1, 2, Fw::Enabled::ENABLED); // SILENCED will not emit packet even while enabled this->component.doDispatch(); - // Redundant Enable to Test silence logic. - this->sendCmd_ENABLE_GROUP(0, 0, 0, 2, Fw::Enabled::ENABLED); + + this->sendCmd_ENABLE_GROUP(0, 0, 2, 2, Fw::Enabled::DISABLED); this->component.doDispatch(); this->clearHistory(); + // Group 3 + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 3, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 0, 7); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 1, 3, Fw::Enabled::ENABLED); + this->component.doDispatch(); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); + this->component.doDispatch(); + this->sendCmd_ENABLE_GROUP(0, 0, 0, 3, Fw::Enabled::ENABLED); + this->component.doDispatch(); + + this->sendCmd_ENABLE_GROUP(0, 0, 2, 3, Fw::Enabled::DISABLED); + this->component.doDispatch(); + this->clearHistory(); /* + Configuration: Port 0 Gorup 1: 3, 15 MIN 3 Port 1 Group 1: 2, 14 MIN 2 - Port 0 Group 2: 1, 4, 13, 16. MIN 1, MAX 12 - Port 1 Group 2: 0, 7, 12, 18. MIN 0, MAX 7 - Port 2 Group 1: 6, 18. MAX 6 - Port 2 group 2 Ignored + Port 0 Group 2: 1, 4, 13, 16. MIN 4, MAX 12 + Port 1 Group 3: 0, 7, 12, 18. MIN 0, MAX 7 + Port 0 Group 3: 6, 18. MAX 6 + Port 1 group 2 Ignored */ - - // Music Melody Testing! - // Expected Behavior: /* + Music Testing! + + T=0 Tests Updates of packets 1,2, and 4 for Groups 1,2, and 4. Updated Packets are emitted. + T=1 Tests Updates of packets 1,2, and 3. Packet 3 is emitted, while Packet 2 is not due to < MIN (Each packet has their own counter) + T=2 Packet 1 emits after passing MIN (configured for port 1, group 1, updated at T=1) + T=3 Packet 1 emits after passing MIN (configured for port 0, group 1, updated at T=1) + T=4 Packet 2 emits after passing MIN (Received update at T=1) + T=4 Test updates packet 2 for group 2. This tests updating a packet when time = MIN, and should be emitted. (Packet 2 and 3 have their own counters) + T=6 Packet 4 emits on port 1 after passing MAX (configured for port 1, group 3). + T=7 Packet 4 emits on port 0 after passing MAX, even if it had received no updates. + + T=12 Tests updating packets 1, 2, and 4. Packet 4 on is emitted since it is updated after MIN and before MAX. Packets 1 and 2 are updated after MIN and may also be at MAX, which is then emitted. + - - T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 T=13 T=14 T=15 T=16 T=17 T=18 T=19 T=20 + Packet Updates 1,2,4 1,2,3 1,2,4 + V V V + T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 - -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| - Port 0 Group 1 O O | | O O | | | - Port 1 Group 1 -O---------------O---------------|-------------------------------|-------------------------------O---------------O---------------|-------------------------------|-------------------------------| - Port 0 Group 2 O O O | O O O | | - Port 1 Group 2 -O-------------------------------|-----------------------O-------|-------------------------------O-------------------------------|-----------------------O-------|-------------------------------| - Port 2 Group 1 O | O | O | O | | - Port 2 Group 2 -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| - | | | | | | | - -|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------|-------------------------------| + (Bass Clef) -|-------------------------------|-------------------------------|-------------------------------|- + Port 0 Group 1 O O | | O + Port 1 Group 1 -O---------------O---------------|-------------------------------|-------------------------------O- + Port 0 Group 2 O O O | O + Port 1 Group 3 -O-------------------------------|-----------------------O-------|-------------------------------O- + Port 0 Group 3 O | O | O + Port 1 Group 2 -|-------------------------------|-------------------------------|-------------------------------|- + | | | | + -|-------------------------------|-------------------------------|-------------------------------|- + | + Note: Packets 2 and 3 are updated and have their own independent counters! + + Expected Output: 5 1 1 1 1 0 1 1 0 0 0 0 5 */ - // Group 1 - // first channel - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(20))); + // 1st Channel (Pkt 1, 2, 4) + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); this->invoke_to_TlmRecv(0, 10, time, buffer); - // second channel + // 2nd Channel (Pkt 1) buffer.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(2))); this->invoke_to_TlmRecv(0, 100, time, buffer); - // third channel + // 3rd Channel (Pkt 1) buffer.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(14))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(3))); this->invoke_to_TlmRecv(0, 333, time, buffer); - - // Group 2 - // 1st channel + // 2nd Channel (Pkt 2) buffer.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1000000))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(2))); this->invoke_to_TlmRecv(0, 13, time, buffer); - // 2nd channel + // 3rd Channel (Pkt 2) buffer.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1010))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(3))); this->invoke_to_TlmRecv(0, 250, time, buffer); - // 3rd channel + // 4th Channel (Pkt 2) buffer.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(4))); this->invoke_to_TlmRecv(0, 22, time, buffer); + // 2nd Channel (Pkt 4) + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(2))); + this->invoke_to_TlmRecv(0, 60, time, buffer); + + // this->m_testTime.add(1, 0); + // this->setTestTime(this->m_testTime); + - this->setTestTime(this->m_testTime); // run scheduler port to send packets + // T=0 + // this->clearFromPortHistory(); this->invoke_to_Run(0, 0); + this->component.doDispatch(); - ASSERT_FROM_PORT_HISTORY_SIZE(2); - ASSERT_from_PktSend_SIZE(2); + ASSERT_FROM_PORT_HISTORY_SIZE(5); + ASSERT_from_PktSend_SIZE(5); // construct the packet buffers and make sure they are correct + // Pkt 1 Fw::ComBuffer comBuff; ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_from_PktSend(0, comBuff, static_cast(0)); + ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // Pkt 2 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + ASSERT_from_PktSend(2, comBuff, static_cast(0)); + + // Pkt 4 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(16))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); + ASSERT_from_PktSend(3, comBuff, static_cast(0)); + ASSERT_from_PktSend(4, comBuff, static_cast(0)); + this->clearHistory(); + + + + // 2nd Channel (Pkt 1) + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(22))); + this->invoke_to_TlmRecv(0, 100, time, buffer); + + // 2nd Channel (Pkt 2) + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(22))); + this->invoke_to_TlmRecv(0, 13, time, buffer); + + // 1st Channel (Pkt 3) + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(11))); + this->invoke_to_TlmRecv(0, 67, time, buffer); + + + // T=1 + this->invoke_to_Run(0, 0); this->component.doDispatch(); - ASSERT_FROM_PORT_HISTORY_SIZE(2); - ASSERT_from_PktSend_SIZE(2); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); - // construct the packet buffers and make sure they are correct + // Pkt 3 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(12))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(11))); + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + this->clearHistory(); + + + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + // Pkt 1 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + this->clearHistory(); + + + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + + // Pkt 1 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + + + // // construct the packet buffers and make sure they are correct + + // comBuff.resetSer(); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, + // comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + + // ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + // comBuff.resetSer(); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, + // comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + + // ASSERT_from_PktSend(1, comBuff, static_cast(0)); } // ---------------------------------------------------------------------- From 7c542a10b0cea0dc29596315238eb4f807526aa2 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Fri, 23 Jan 2026 13:29:32 -0600 Subject: [PATCH 05/16] Passing UT --- Svc/TlmPacketizer/TlmPacketizer.cpp | 7 +- .../test/ut/TlmPacketizerTester.cpp | 160 +++++++++++++----- .../test/ut/TlmPacketizerTester.hpp | 4 +- 3 files changed, 129 insertions(+), 42 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index b6e11173596..1b79eab6d2d 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -394,7 +394,10 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // If counter is less than delta min } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { // Keep flag true but do not send. - pktEntryFlags.prevSentCounter++; + if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) + { + pktEntryFlags.prevSentCounter++; + } continue; } } @@ -427,9 +430,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { Fw::Time::SERIALIZED_SIZE); Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); - this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); - // printf("PKT SENT %d PORT %d\n", pkt, port); pktEntryFlags.prevSentCounter = 0; pktEntryFlags.updateFlag = false; } diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index abc63883c8e..a51f52e15ab 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -1048,11 +1048,11 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 (Bass Clef) -|-------------------------------|-------------------------------|-------------------------------|- - Port 0 Group 1 O O | | O - Port 1 Group 1 -O---------------O---------------|-------------------------------|-------------------------------O- - Port 0 Group 2 O O O | O - Port 1 Group 3 -O-------------------------------|-----------------------O-------|-------------------------------O- - Port 0 Group 3 O | O | O + Port 0 Group 1 ● ● | | ● + Port 1 Group 1 -●---------------●---------------|-------------------------------|-------------------------------●- + Port 0 Group 2 ● ● ● | ● + Port 1 Group 3 -●-------------------------------|-----------------------●-------|-------------------------------●- + Port 0 Group 3 ● | ● | ● Port 1 Group 2 -|-------------------------------|-------------------------------|-------------------------------|- | | | | -|-------------------------------|-------------------------------|-------------------------------|- @@ -1096,13 +1096,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(2))); this->invoke_to_TlmRecv(0, 60, time, buffer); - // this->m_testTime.add(1, 0); - // this->setTestTime(this->m_testTime); - - // run scheduler port to send packets - // T=0 - // this->clearFromPortHistory(); + // T = 0 this->invoke_to_Run(0, 0); this->component.doDispatch(); @@ -1170,8 +1165,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->invoke_to_TlmRecv(0, 67, time, buffer); - // T=1 - + // T = 1 this->invoke_to_Run(0, 0); this->component.doDispatch(); @@ -1189,7 +1183,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->clearHistory(); - + // T = 2 this->invoke_to_Run(0, 0); this->component.doDispatch(); @@ -1206,55 +1200,145 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); + // Pkt 1 on Port 1 ASSERT_from_PktSend(0, comBuff, static_cast(0)); this->clearHistory(); + // T = 3 + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + + // Pkt 1 on Port 0 + // comBuff unchanged since this->m_testTime is the same + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + this->clearHistory(); + // T = 4 this->invoke_to_Run(0, 0); this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + // Pkt 2 on Port 0 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + this->clearHistory(); + + // T = 5 + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + // Not expecting any packets + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); + + // T = 6 + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); - // Pkt 1 + // Pkt 4 on Port 1 (Unchanged since T = 0) comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(16))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); ASSERT_from_PktSend(0, comBuff, static_cast(0)); + this->clearHistory(); + + // T = 7 + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + + // Pkt 4 on Port 0 (Unchanged since T = 0) + ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + this->clearHistory(); + // T = 8-11, Expecting No Updates + for (U8 trial = 8; trial < 12; trial++) { + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); + } - // // construct the packet buffers and make sure they are correct + buffer.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(111))); + this->invoke_to_TlmRecv(0, 10, time, buffer); + + this->clearHistory(); + + // T = 12 + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(5); + ASSERT_from_PktSend_SIZE(5); + + // Pkt 1 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(111))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - // comBuff.resetSer(); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, - // comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); + ASSERT_from_PktSend(0, comBuff, static_cast(0)); // Port 0 + ASSERT_from_PktSend(1, comBuff, static_cast(0)); // Port 1 - // ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 2 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(111))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - // comBuff.resetSer(); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, - // comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); + ASSERT_from_PktSend(2, comBuff, static_cast(0)); // Port 0 + + // Pkt 4 + comBuff.resetSer(); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, + comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(16))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(111))); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); - // ASSERT_from_PktSend(1, comBuff, static_cast(0)); + ASSERT_from_PktSend(3, comBuff, static_cast(0)); // Port 0 + ASSERT_from_PktSend(4, comBuff, static_cast(0)); // Port 1 } // ---------------------------------------------------------------------- diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp index 08972313653..b48ed56c933 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp @@ -131,7 +131,9 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { Fw::Time m_testTime; //!< store test time for packets - bool m_port0Lock{true}; //! Lock limited to entries from port 0 PktSend + bool m_port0Lock{true}; //! Lock limited to entries from port 0 PktSend + + // Svc::Queue m_portCalls{}; }; } // end namespace Svc From 3081d380737e145762c8c961fe1c78b6210b3c9c Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Fri, 23 Jan 2026 15:46:08 -0600 Subject: [PATCH 06/16] UT Impl Changes --- Svc/TlmPacketizer/TlmPacketizer.cpp | 54 ++++++++++--------- .../test/ut/TlmPacketizerTester.cpp | 43 ++++++++------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 1b79eab6d2d..36185d239b4 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -106,10 +106,19 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, if (packetList.list[pktEntry]->level > this->m_maxLevel) { this->m_maxLevel = packetList.list[pktEntry]->level; } - // save start level - this->m_startLevel = startLevel; + } // end packet list + + // save start level + this->m_startLevel = startLevel; + + // enable / disable appropriate groups + for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { + for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + this->m_groupConfigs[port][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + } + } // populate hash table with ignore list for (FwChanIdType channelEntry = 0; channelEntry < ignoreList.numEntries; channelEntry++) { @@ -338,8 +347,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { this->m_lock.lock(); // copy buffers from fill side to send side for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { - if ((this->m_fillBuffers[pkt].updated) and - ((this->m_fillBuffers[pkt].level <= this->m_startLevel) or (this->m_fillBuffers[pkt].requested))) { + if ((this->m_fillBuffers[pkt].updated) or (this->m_fillBuffers[pkt].requested)) { this->m_sendBuffers[pkt] = this->m_fillBuffers[pkt]; if (PACKET_UPDATE_ON_CHANGE == PACKET_UPDATE_MODE) { this->m_fillBuffers[pkt].updated = false; @@ -371,19 +379,19 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Iterate through output ports for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { - PktSendCounters& pktEntryFlags = this->m_packetFlags[port][pkt]; - // printf("TESTING PKT %d PORT %d\n", pkt, port); if (not this->isConnected_PktSend_OutputPort(port)) { continue; } + PktSendCounters& pktEntryFlags = this->m_packetFlags[port][pkt]; + if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF){ + pktEntryFlags.prevSentCounter++; + } + if (entryGroup <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP and not this->m_sendBuffers[pkt].requested) { GroupConfig& entryGroupConfig = this->m_groupConfigs[port][entryGroup]; - if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) - { - continue; - } + // If a packet in the group has been updated since last sent on port if (pktEntryFlags.updateFlag) { @@ -394,10 +402,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // If counter is less than delta min } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { // Keep flag true but do not send. - if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) - { - pktEntryFlags.prevSentCounter++; - } + continue; } } @@ -410,18 +415,16 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { pktEntryFlags.updateFlag = true; } - if (not (entryGroupConfig.enabled or entryGroupConfig.forceEnabled)) { + if (not (entryGroupConfig.enabled or entryGroupConfig.forceEnabled) or + entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) { pktEntryFlags.updateFlag = false; } } - // Send under the following conditions: - // 1. Packet received updates and it has been past delta min counts since last packet (and min is configured) - // 2. Packet has passed delta max counts since last packet (and max is configured) + // 1. Packet received updates and it has been past delta min counts since last packet (min enabled) + // 2. Packet has passed delta max counts since last packet (max enabled) // With the above, the group must be either enabled or force enabled. - // 3. If the group of the packet lies above the max configurable tlmpacketizer group, update behavior is - // determined fully on updates or emitted always. - // 4. If the packet was requested. + // 3. If the packet was requested. if (pktEntryFlags.updateFlag) { // serialize time into time offset in packet Fw::ExternalSerializeBuffer buff( @@ -434,10 +437,6 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { pktEntryFlags.prevSentCounter = 0; pktEntryFlags.updateFlag = false; } - if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) - { - pktEntryFlags.prevSentCounter++; - } } this->m_sendBuffers[pkt].requested = false; } @@ -457,6 +456,11 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c if (level > this->m_maxLevel) { this->log_WARNING_LO_MaxLevelExceed(level, this->m_maxLevel); } + for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { + for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + this->m_groupConfigs[port][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + } + } this->tlmWrite_SendLevel(level); this->log_ACTIVITY_HI_LevelSet(level); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index a51f52e15ab..d4d126e41d5 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -956,32 +956,38 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->component.setPacketList(packetList2, ignore, 4); this->m_port0Lock = false; Fw::Time time; - // time = this->m_testTime; - // time.set(0, 0); - // this->m_testTime = time; - // this->setTestTime(this->m_testTime); - Fw::TlmBuffer buffer; + this->sendCmd_SET_LEVEL(0, 0, 0); + this->component.doDispatch(); + + // 1st Channel (Pkt 1, 2, 4) + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(0xFFFF))); + this->invoke_to_TlmRecv(0, 10, time, buffer); + + // T = -1 (Check to see that setting level 0 does not emit packets) + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); - // this->invoke_to_Run(0, 0); - // this->component.doDispatch(); - // ASSERT_from_PktSend_SIZE(0); + this->clearHistory(); // time.set(100, 1000); // Arguments: opCode, cmdSeq, portOut, tlmGroup, minDelta, maxDelta + // Set level to 3 to enable all levels + this->sendCmd_SET_LEVEL(0, 0, 10); + this->component.doDispatch(); + // Group 1 this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 3, 3); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::ENABLED); - this->component.doDispatch(); // Send every 5 on port 1 this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 2, 2); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 1, 1, Fw::Enabled::ENABLED); - this->component.doDispatch(); // Disable on Port 2 this->sendCmd_ENABLE_GROUP(0, 0, 2, 1, Fw::Enabled::DISABLED); @@ -991,8 +997,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // Group 2 this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 4, 12); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 0, 2, Fw::Enabled::ENABLED); - this->component.doDispatch(); this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); this->component.doDispatch(); @@ -1006,16 +1010,18 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // Group 3 this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 3, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 0, 7); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 1, 3, Fw::Enabled::ENABLED); - this->component.doDispatch(); this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 0, 3, Fw::Enabled::ENABLED); + this->sendCmd_ENABLE_GROUP(0, 0, 0, 3, Fw::Enabled::DISABLED); + this->component.doDispatch(); + this->sendCmd_FORCE_GROUP(0, 0, 0, 3, Fw::Enabled::ENABLED); // Force Group enables this group on port 0 this->component.doDispatch(); this->sendCmd_ENABLE_GROUP(0, 0, 2, 3, Fw::Enabled::DISABLED); this->component.doDispatch(); + + this->clearHistory(); /* @@ -1062,7 +1068,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { Expected Output: 5 1 1 1 1 0 1 1 0 0 0 0 5 */ + // 1st Channel (Pkt 1, 2, 4) + buffer.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); this->invoke_to_TlmRecv(0, 10, time, buffer); @@ -1364,7 +1372,6 @@ void TlmPacketizerTester ::connectPorts() { // PktSend this->component.set_PktSend_OutputPort(0, this->get_from_PktSend(0)); this->component.set_PktSend_OutputPort(1, this->get_from_PktSend(1)); - this->component.set_PktSend_OutputPort(2, this->get_from_PktSend(2)); // Run this->connect_to_Run(0, this->component.get_Run_InputPort(0)); From 57c7c2f4db0bb1dd973dab24fa37443437ec9957 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Mon, 26 Jan 2026 16:35:31 -0600 Subject: [PATCH 07/16] Changed ports to sections --- Svc/TlmPacketizer/TlmPacketizer.cpp | 46 ++++++++++--------- Svc/TlmPacketizer/TlmPacketizer.fpp | 25 ++++------ Svc/TlmPacketizer/TlmPacketizer.hpp | 10 ++-- Svc/TlmPacketizer/docs/sdd.md | 9 ++++ .../test/ut/TlmPacketizerTester.cpp | 17 ++++--- .../test/ut/TlmPacketizerTester.hpp | 2 +- default/config/TlmPacketizerCfg.fpp | 3 ++ 7 files changed, 58 insertions(+), 54 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 36185d239b4..7f1120e0210 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -114,9 +114,9 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, this->m_startLevel = startLevel; // enable / disable appropriate groups - for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { - this->m_groupConfigs[port][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } } @@ -376,24 +376,26 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { FwChanIdType entryGroup = this->m_sendBuffers[pkt].level; - // Iterate through output ports - for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) + // Iterate through output prioritys + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) { - if (not this->isConnected_PktSend_OutputPort(port)) + FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + pkt); + if (not this->isConnected_PktSend_OutputPort(outIndex)) { + printf("SKIPPED PACKET %d\n", pkt); continue; } - PktSendCounters& pktEntryFlags = this->m_packetFlags[port][pkt]; + PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF){ pktEntryFlags.prevSentCounter++; } if (entryGroup <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP and not this->m_sendBuffers[pkt].requested) { - GroupConfig& entryGroupConfig = this->m_groupConfigs[port][entryGroup]; + GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; - // If a packet in the group has been updated since last sent on port + // If a packet in the group has been updated since last sent on section if (pktEntryFlags.updateFlag) { // If delta min is disabled (Disable on change, Only send on Delta Max) if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::EVERY_MAX) { @@ -407,11 +409,11 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } } - // If Delta Max is configured and the send counter for this port and packet is greater than Delta Max + // If Delta Max is configured and the send counter for this section and packet is greater than Delta Max if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and pktEntryFlags.prevSentCounter >= entryGroupConfig.max) { - // Signal an update if counter exceeded max counts for current port + // Signal an update if counter exceeded max counts for current section pktEntryFlags.updateFlag = true; } @@ -433,7 +435,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { Fw::Time::SERIALIZED_SIZE); Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); - this->PktSend_out(port, this->m_sendBuffers[pkt].buffer, 0); + this->PktSend_out(outIndex, this->m_sendBuffers[pkt].buffer, 0); pktEntryFlags.prevSentCounter = 0; pktEntryFlags.updateFlag = false; } @@ -456,9 +458,9 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c if (level > this->m_maxLevel) { this->log_WARNING_LO_MaxLevelExceed(level, this->m_maxLevel); } - for (FwIndexType port = 0; port < NUM_PKTSEND_OUTPUT_PORTS; port++) { + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { - this->m_groupConfigs[port][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } } this->tlmWrite_SendLevel(level); @@ -493,41 +495,41 @@ void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cm void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, - FwIndexType portOut, + FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (portOut > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - this->m_groupConfigs[portOut][tlmGroup].enabled = enable; + this->m_groupConfigs[section][tlmGroup].enabled = enable; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, - FwIndexType portOut, + FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (portOut > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - this->m_groupConfigs[portOut][tlmGroup].forceEnabled = enable; + this->m_groupConfigs[section][tlmGroup].forceEnabled = enable; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, - FwIndexType portOut, + FwIndexType section, FwChanIdType tlmGroup, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (portOut > NUM_PKTSEND_OUTPUT_PORTS or + if (section > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP or minDelta > maxDelta) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - GroupConfig& groupConfig = this->m_groupConfigs[portOut][tlmGroup]; + GroupConfig& groupConfig = this->m_groupConfigs[section][tlmGroup]; groupConfig.rateLogic = rateLogic; groupConfig.min = minDelta; groupConfig.max = maxDelta; diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index eb730d87618..d6044e7ba16 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -7,12 +7,13 @@ module Svc { ON_CHANGE_MIN, ON_CHANGE_MIN_AND_EVERY_MAX, } + # ---------------------------------------------------------------------- # General ports # ---------------------------------------------------------------------- @ Packet send port - output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS] Fw.Com + output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)] Fw.Com @ Ping input port async input port pingIn: Svc.Ping @@ -54,12 +55,6 @@ module Svc { @ Time get time get port timeGetOut - @ Param Get - param get port prmGetOut - - @ Param Set - param set port prmSetOut - # ---------------------------------------------------------------------- # Commands # ---------------------------------------------------------------------- @@ -76,22 +71,25 @@ module Svc { ) \ opcode 1 + @ Enable / disable telemetry of a group on a specific port async command ENABLE_GROUP( - portOut: FwIndexType @< Port to configure + section: FwIndexType @< section grouping to configure tlmGroup: FwChanIdType @< Group Level enable: Fw.Enabled @< Active Sending Group ) \ opcode 2 + @ Force telemetering a group on a port, even if disabled async command FORCE_GROUP( - portOut: FwIndexType @< Port to configure + section: FwIndexType @< section grouping tlmGroup: FwChanIdType @< Group Level enable: Fw.Enabled @< Active Sending Group ) \ opcode 3 + @ Set Min and Max Deltas between successive packets async command SET_GROUP_DELTAS( - portOut: FwIndexType @< Port to configure + section: FwIndexType @< section grouping tlmGroup: FwChanIdType @< Group Level rateLogic: RateLogic @< Rate Logic minDelta: U32 @@ -152,13 +150,6 @@ module Svc { @ Telemetry send level telemetry SendLevel: FwChanIdType id 0 - - # ---------------------------------------------------------------------- - # Params - # ---------------------------------------------------------------------- - - array ForceConfigurations = [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS] Fw.Active default Fw.Active.ACTIVE - param FORCE_OUT: ForceConfigurations } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 6a7810f4d31..0de0d6280a0 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -92,7 +92,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Handler implementation for command ENABLE_GROUP void ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType portOut, //!< Port to configure + FwIndexType section, //!< Port to configure FwChanIdType tlmGroup, //!< Group Level Fw::Enabled enable //!< Active Sending Group ) override; @@ -100,7 +100,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Handler implementation for command FORCE_GROUP void FORCE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType portOut, //!< Port to configure + FwIndexType section, //!< Port to configure FwChanIdType tlmGroup, //!< Group Level Fw::Enabled enable //!< Active Sending Group ) override; @@ -108,7 +108,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Handler implementation for command SET_GROUP_DELTAS void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType portOut, //!< Port to configure + FwIndexType section, //!< Port to configure FwChanIdType tlmGroup, //!< Group Level Svc::TlmPacketizer_RateLogic rateLogic, //!< Rate Logic U32 minDelta, @@ -178,12 +178,12 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; U32 min = 0; //!< Default for Backwards Compatible Behavior U32 max = 0; //!< Default for Backwards Compatible Behavior - } m_groupConfigs[NUM_PKTSEND_OUTPUT_PORTS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; + } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; struct PktSendCounters { U32 prevSentCounter = 0xFFFFFFFF; bool updateFlag = false; - } m_packetFlags[NUM_PKTSEND_OUTPUT_PORTS][MAX_PACKETIZER_PACKETS]{}; + } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{}; }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/docs/sdd.md b/Svc/TlmPacketizer/docs/sdd.md index 894c3d21a63..085ea7c5621 100644 --- a/Svc/TlmPacketizer/docs/sdd.md +++ b/Svc/TlmPacketizer/docs/sdd.md @@ -4,6 +4,8 @@ The `Svc::TlmPacketizer` Component is used to store telemetry values written by other components. The values are stored in serialized form. TlmPacketizer differs from `Svc::TlmChan` in that it stores telemetry in defined packets instead of streaming the updates as they come. The defined packets are passed in as a table to the `setPacketList()` public method. When telemetry updates are passed to the component, they are placed at the offset in a packet buffer defined by the table. When the `run()` port is called, all the defined packets are sent to the output port with the most recent . This is meant to replace `Svc::TlmCham` for use cases where a more compact packet format is desired. The disadvantage is that all channels are pushed whether or not they have been updated. +Uses can change the individual rates at which groups per port instance are outputted upon a `run()` port call. Groups are configured with a MIN number of `run()` invocations to emit updated telemetry, and a MAX number of `run()` invocations that shall output a packet if it exceeds MAX. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. + ## 2. Requirements The requirements for `Svc::TlmPacketizer` are as follows: @@ -49,6 +51,12 @@ When a call to the `Run()` interface is called, the packet writes are locked and #### 3.3.1 External User Option +#### 3.3.2 Configuring Telemetry Group Rates Per Port + +The `Svc::TlmPacketizer` is configurable to have multiple `PktSend` ports using the fpp constant, `MAX_CONFIGURABLE_TLMPACKETIZER_PORTS`. Doing so allows each packet group have different configurations for each `PktSend` output port. + + + ### 3.4 State `Svc::TlmPacketizer` has no state machines. @@ -71,4 +79,5 @@ To see unit test coverage run fprime-util check --coverage Date | Description ---- | ----------- 12/14/2017 | Initial version +01/23/2026 | Added group level rate logic diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index d4d126e41d5..6603e249be1 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -26,7 +26,6 @@ TlmPacketizerTester ::TlmPacketizerTester() : TlmPacketizerGTestBase("Tester", MAX_HISTORY_SIZE), component("TlmPacketizer") { this->initComponents(); this->connectPorts(); - this->component.loadParameters(); } TlmPacketizerTester ::~TlmPacketizerTester() { @@ -954,7 +953,7 @@ void TlmPacketizerTester ::getChannelValueTest() { //! void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->component.setPacketList(packetList2, ignore, 4); - this->m_port0Lock = false; + this->m_primaryTestLock = false; Fw::Time time; Fw::TlmBuffer buffer; this->sendCmd_SET_LEVEL(0, 0, 0); @@ -975,7 +974,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // time.set(100, 1000); - // Arguments: opCode, cmdSeq, portOut, tlmGroup, minDelta, maxDelta + // Arguments: opCode, cmdSeq, section, tlmGroup, minDelta, maxDelta // Set level to 3 to enable all levels this->sendCmd_SET_LEVEL(0, 0, 10); @@ -1354,7 +1353,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // ---------------------------------------------------------------------- void TlmPacketizerTester ::from_PktSend_handler(const FwIndexType portNum, Fw::ComBuffer& data, U32 context) { - if (this->m_port0Lock && portNum != 0) { + if (this->m_primaryTestLock && portNum > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP * 1) { return; } this->pushFromPortEntry_PktSend(data, context); @@ -1370,8 +1369,8 @@ void TlmPacketizerTester ::from_pingOut_handler(const FwIndexType portNum, U32 k void TlmPacketizerTester ::connectPorts() { // PktSend - this->component.set_PktSend_OutputPort(0, this->get_from_PktSend(0)); - this->component.set_PktSend_OutputPort(1, this->get_from_PktSend(1)); + // this->component.set_PktSend_OutputPort(0, this->get_from_PktSend(0)); + // this->component.set_PktSend_OutputPort(1, this->get_from_PktSend(1)); // Run this->connect_to_Run(0, this->component.get_Run_InputPort(0)); @@ -1409,9 +1408,9 @@ void TlmPacketizerTester ::connectPorts() { // TlmGet this->connect_to_TlmGet(0, this->component.get_TlmGet_InputPort(0)); - // Params - this->component.set_prmGetOut_OutputPort(0, this->get_from_prmGetOut(0)); - this->component.set_prmSetOut_OutputPort(0, this->get_from_prmSetOut(0)); + for (FwIndexType index = 0; index < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1); index++) { + this->component.set_PktSend_OutputPort(index, this->get_from_PktSend(index)); + } } void TlmPacketizerTester::textLogIn(const FwEventIdType id, //!< The event ID diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp index b48ed56c933..2899c2dfc07 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp @@ -131,7 +131,7 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { Fw::Time m_testTime; //!< store test time for packets - bool m_port0Lock{true}; //! Lock limited to entries from port 0 PktSend + bool m_primaryTestLock{true}; //! Lock limited to entries from port 0 PktSend // Svc::Queue m_portCalls{}; }; diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp index 9faafc467be..b022ff6f999 100644 --- a/default/config/TlmPacketizerCfg.fpp +++ b/default/config/TlmPacketizerCfg.fpp @@ -3,6 +3,9 @@ # Constants for configuring TlmPacketizer Send Logic Levels # ====================================================================== module Svc { + @ The number of sections of ports (Primary = 0, Secondary = 1, etc...) constant NUM_CONFIGURABLE_TLMPACKETIZER_PORTS = 3; + + @ Greatest packet group (inclusive) constant MAX_CONFIGURABLE_TLMPACKETIZER_GROUP = 3; } From a23a0ef7ec63e08fa81a86ddbbbc063fd326def6 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Mon, 26 Jan 2026 18:06:45 -0600 Subject: [PATCH 08/16] Control Adds --- Svc/TlmPacketizer/TlmPacketizer.cpp | 34 +++++++++++++------ Svc/TlmPacketizer/TlmPacketizer.fpp | 21 ++++++++++-- Svc/TlmPacketizer/TlmPacketizer.hpp | 29 ++++++++++------ .../test/ut/TlmPacketizerTester.cpp | 19 +++++------ default/config/TlmPacketizerCfg.fpp | 2 +- 5 files changed, 69 insertions(+), 36 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 7f1120e0210..3b3f3c36bef 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -46,6 +46,12 @@ TlmPacketizer ::TlmPacketizer(const char* const compName) this->m_fillBuffers[buffer].requested = false; this->m_sendBuffers[buffer].updated = false; } + + // clear enabled sections + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { + this->m_sectionEnabled[section] = Fw::Enabled::ENABLED; + this->m_forceEnabled[section] = Fw::Enabled::DISABLED; + } } TlmPacketizer ::~TlmPacketizer() {} @@ -114,7 +120,7 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, this->m_startLevel = startLevel; // enable / disable appropriate groups - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) { + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } @@ -364,7 +370,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Update per port group flags if (this->m_sendBuffers[pkt].updated == true) { - for (FwIndexType port = 0; port < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; port++) { + for (FwIndexType port = 0; port < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; port++) { this->m_packetFlags[port][pkt].updateFlag = true; } this->m_sendBuffers[pkt].updated = false; @@ -377,12 +383,11 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FwChanIdType entryGroup = this->m_sendBuffers[pkt].level; // Iterate through output prioritys - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + pkt); if (not this->isConnected_PktSend_OutputPort(outIndex)) { - printf("SKIPPED PACKET %d\n", pkt); continue; } @@ -404,7 +409,6 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // If counter is less than delta min } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { // Keep flag true but do not send. - continue; } } @@ -417,7 +421,8 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { pktEntryFlags.updateFlag = true; } - if (not (entryGroupConfig.enabled or entryGroupConfig.forceEnabled) or + if (not ((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or + this->m_forceEnabled[section] == Fw::Enabled::ENABLED) or entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) { pktEntryFlags.updateFlag = false; } @@ -444,6 +449,14 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } } +void TlmPacketizer ::controlIn_handler(FwIndexType portNum, FwIndexType section, const Fw::Enabled& enabled) { + if (0 <= section && section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) { + this->m_sectionEnabled[section] = enabled; + } else { + this->log_WARNING_LO_SectionUnconfigurable(section, enabled); + } +} + void TlmPacketizer ::pingIn_handler(const FwIndexType portNum, U32 key) { // return key this->pingOut_out(0, key); @@ -458,7 +471,7 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c if (level > this->m_maxLevel) { this->log_WARNING_LO_MaxLevelExceed(level, this->m_maxLevel); } - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS; section++) { + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } @@ -505,15 +518,14 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, +void TlmPacketizer ::FORCE_SECTION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwIndexType section, - FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section > NUM_PKTSEND_OUTPUT_PORTS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); } - this->m_groupConfigs[section][tlmGroup].forceEnabled = enable; + this->m_forceEnabled[section] = enable; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index d6044e7ba16..6ea4500d466 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -1,4 +1,9 @@ module Svc { + port EnableSection ( + section: FwIndexType @< Section to enable (Primary, Secondary, etc...) + enabled: Fw.Enabled @< Enable / Disable Section + ) + @ A component for storing telemetry active component TlmPacketizer { enum RateLogic { @@ -7,13 +12,16 @@ module Svc { ON_CHANGE_MIN, ON_CHANGE_MIN_AND_EVERY_MAX, } + # ---------------------------------------------------------------------- # General ports # ---------------------------------------------------------------------- @ Packet send port - output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_PORTS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)] Fw.Com + output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)] Fw.Com + + async input port controlIn: EnableSection @ Ping input port async input port pingIn: Svc.Ping @@ -80,9 +88,8 @@ module Svc { opcode 2 @ Force telemetering a group on a port, even if disabled - async command FORCE_GROUP( + async command FORCE_SECTION( section: FwIndexType @< section grouping - tlmGroup: FwChanIdType @< Group Level enable: Fw.Enabled @< Active Sending Group ) \ opcode 3 @@ -144,6 +151,14 @@ module Svc { id 4 \ format "Could not find packet ID {}" + event SectionUnconfigurable( + ection: FwIndexType @< The Section + enable: Fw.Enabled @< Attempted Configuration + ) \ + severity warning low \ + id 5 \ + format "Section {} is unconfigurable and cannot be set to {}" + # ---------------------------------------------------------------------- # Telemetry # ---------------------------------------------------------------------- diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 0de0d6280a0..875dc271ce4 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -61,6 +61,12 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { U32 context /*!< The call order*/ ) override; + //! Handler implementation for controlIn + void controlIn_handler(FwIndexType portNum, //!< The port number + FwIndexType section, //!< Section to enable (Primary, Secondary, etc...) + const Fw::Enabled& enabled //!< Enable / Disable Section + ) override; + //! Handler implementation for pingIn //! void pingIn_handler(const FwIndexType portNum, /*!< The port number*/ @@ -92,23 +98,22 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Handler implementation for command ENABLE_GROUP void ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Port to configure + FwIndexType section, //!< Section to configure FwChanIdType tlmGroup, //!< Group Level Fw::Enabled enable //!< Active Sending Group ) override; //! Handler implementation for command FORCE_GROUP - void FORCE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Port to configure - FwChanIdType tlmGroup, //!< Group Level - Fw::Enabled enable //!< Active Sending Group - ) override; + void FORCE_SECTION_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType section, //!< Section to configure + Fw::Enabled enable //!< Active Sending Group + ) override; //! Handler implementation for command SET_GROUP_DELTAS void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Port to configure + FwIndexType section, //!< Section to configure FwChanIdType tlmGroup, //!< Group Level Svc::TlmPacketizer_RateLogic rateLogic, //!< Rate Logic U32 minDelta, @@ -174,16 +179,18 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { struct GroupConfig { Fw::Enabled enabled = Fw::Enabled::ENABLED; - Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; U32 min = 0; //!< Default for Backwards Compatible Behavior U32 max = 0; //!< Default for Backwards Compatible Behavior - } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; + } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; + + Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; + Fw::Enabled m_forceEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; struct PktSendCounters { U32 prevSentCounter = 0xFFFFFFFF; bool updateFlag = false; - } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_PORTS][MAX_PACKETIZER_PACKETS]{}; + } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 6603e249be1..cb0c7c89c49 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -988,9 +988,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 2, 2); this->component.doDispatch(); - // Disable on Port 2 - this->sendCmd_ENABLE_GROUP(0, 0, 2, 1, Fw::Enabled::DISABLED); - this->component.doDispatch(); this->clearHistory(); // Group 2 @@ -1002,8 +999,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_ENABLE_GROUP(0, 0, 1, 2, Fw::Enabled::ENABLED); // SILENCED will not emit packet even while enabled this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 2, 2, Fw::Enabled::DISABLED); - this->component.doDispatch(); this->clearHistory(); // Group 3 @@ -1014,13 +1009,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->component.doDispatch(); this->sendCmd_ENABLE_GROUP(0, 0, 0, 3, Fw::Enabled::DISABLED); this->component.doDispatch(); - this->sendCmd_FORCE_GROUP(0, 0, 0, 3, Fw::Enabled::ENABLED); // Force Group enables this group on port 0 + + this->sendCmd_FORCE_SECTION(0, 0, 0, Fw::Enabled::ENABLED); // Force telemetry on section 0 for all groups, regardless if disabled this->component.doDispatch(); - - this->sendCmd_ENABLE_GROUP(0, 0, 2, 3, Fw::Enabled::DISABLED); + + // Disable output on section 2 + this->invoke_to_controlIn(0, 2, Fw::Enabled::DISABLED); this->component.doDispatch(); - this->clearHistory(); /* @@ -1408,9 +1404,12 @@ void TlmPacketizerTester ::connectPorts() { // TlmGet this->connect_to_TlmGet(0, this->component.get_TlmGet_InputPort(0)); - for (FwIndexType index = 0; index < NUM_CONFIGURABLE_TLMPACKETIZER_PORTS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1); index++) { + for (FwIndexType index = 0; index < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1); index++) { this->component.set_PktSend_OutputPort(index, this->get_from_PktSend(index)); } + + // controlIn + this->connect_to_controlIn(0, this->component.get_controlIn_InputPort(0)); } void TlmPacketizerTester::textLogIn(const FwEventIdType id, //!< The event ID diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp index b022ff6f999..7680d67447b 100644 --- a/default/config/TlmPacketizerCfg.fpp +++ b/default/config/TlmPacketizerCfg.fpp @@ -4,7 +4,7 @@ # ====================================================================== module Svc { @ The number of sections of ports (Primary = 0, Secondary = 1, etc...) - constant NUM_CONFIGURABLE_TLMPACKETIZER_PORTS = 3; + constant NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS = 3; @ Greatest packet group (inclusive) constant MAX_CONFIGURABLE_TLMPACKETIZER_GROUP = 3; From 8f9f5737d6cf5a5382685f24c44f86fc4521cbf3 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Tue, 27 Jan 2026 17:57:40 -0600 Subject: [PATCH 09/16] UT Fixes --- .../CdhCoreConfig/CdhCoreTlmConfig.fpp | 38 +-- Svc/TlmPacketizer/TlmPacketizer.cpp | 35 ++- Svc/TlmPacketizer/TlmPacketizer.fpp | 20 +- Svc/TlmPacketizer/TlmPacketizer.hpp | 17 +- .../test/ut/TlmPacketizerTester.cpp | 248 ++++++++++++++---- .../test/ut/TlmPacketizerTester.hpp | 11 +- Svc/TlmPacketizer/test/ut/main.cpp | 5 + 7 files changed, 287 insertions(+), 87 deletions(-) diff --git a/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp b/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp index 52adeb96f6a..40d6d47bdc9 100644 --- a/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp +++ b/Svc/Subtopologies/CdhCore/CdhCoreConfig/CdhCoreTlmConfig.fpp @@ -1,24 +1,24 @@ module CdhCore{ - # instance tlmSend: Svc.TlmChan base id CdhCoreConfig.BASE_ID + 0x06000 \ - # queue size CdhCoreConfig.QueueSizes.tlmSend \ - # stack size CdhCoreConfig.StackSizes.tlmSend \ - # priority CdhCoreConfig.Priorities.tlmSend \ + instance tlmSend: Svc.TlmChan base id CdhCoreConfig.BASE_ID + 0x06000 \ + queue size CdhCoreConfig.QueueSizes.tlmSend \ + stack size CdhCoreConfig.StackSizes.tlmSend \ + priority CdhCoreConfig.Priorities.tlmSend \ # Uncomment the following block and comment the above block to use TlmPacketizer instead of TlmChan - instance tlmSend: Svc.TlmPacketizer base id CdhCoreConfig.BASE_ID + 0x06000 \ - queue size CdhCoreConfig.QueueSizes.tlmSend \ - stack size CdhCoreConfig.StackSizes.tlmSend \ - priority CdhCoreConfig.Priorities.tlmSend \ - { - # NOTE: The Name Ref is specific to the Reference deployment, Ref - # This name will need to be updated if wishing to use this in a custom deployment - phase Fpp.ToCpp.Phases.configComponents """ - CdhCore::tlmSend.setPacketList( - TestDeployment::TestDeployment_TestDeploymentPacketsTlmPackets::packetList, - TestDeployment::TestDeployment_TestDeploymentPacketsTlmPackets::omittedChannels, - 1 - ); - """ - } + # instance tlmSend: Svc.TlmPacketizer base id CdhCoreConfig.BASE_ID + 0x06000 \ + # queue size CdhCoreConfig.QueueSizes.tlmSend \ + # stack size CdhCoreConfig.StackSizes.tlmSend \ + # priority CdhCoreConfig.Priorities.tlmSend \ + # { + # # NOTE: The Name Ref is specific to the Reference deployment, Ref + # # This name will need to be updated if wishing to use this in a custom deployment + # phase Fpp.ToCpp.Phases.configComponents """ + # CdhCore::tlmSend.setPacketList( + # Ref::Ref_RefPacketsTlmPackets::packetList, + # Ref::Ref_RefPacketsTlmPackets::omittedChannels, + # 1 + # ); + # """ + # } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 3b3f3c36bef..d7de883c724 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -50,7 +50,6 @@ TlmPacketizer ::TlmPacketizer(const char* const compName) // clear enabled sections for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { this->m_sectionEnabled[section] = Fw::Enabled::ENABLED; - this->m_forceEnabled[section] = Fw::Enabled::DISABLED; } } @@ -385,7 +384,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Iterate through output prioritys for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + pkt); + FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + entryGroup); if (not this->isConnected_PktSend_OutputPort(outIndex)) { continue; @@ -422,7 +421,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } if (not ((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - this->m_forceEnabled[section] == Fw::Enabled::ENABLED) or + entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED) or entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) { pktEntryFlags.updateFlag = false; } @@ -506,27 +505,42 @@ void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cm this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } +void TlmPacketizer ::ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwIndexType section, + Fw::Enabled enable) { + if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + this->m_sectionEnabled[section] = enable; + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section > NUM_PKTSEND_OUTPUT_PORTS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; } this->m_groupConfigs[section][tlmGroup].enabled = enable; - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TlmPacketizer ::FORCE_SECTION_cmdHandler(FwOpcodeType opCode, +void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwIndexType section, + FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section > NUM_PKTSEND_OUTPUT_PORTS) { + if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; } - this->m_forceEnabled[section] = enable; - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + this->m_groupConfigs[section][tlmGroup].forceEnabled = enable; + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, @@ -536,10 +550,11 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (section > NUM_PKTSEND_OUTPUT_PORTS or + if (section > NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP or minDelta > maxDelta) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; } GroupConfig& groupConfig = this->m_groupConfigs[section][tlmGroup]; groupConfig.rateLogic = rateLogic; diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 6ea4500d466..3fd44043148 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -79,20 +79,28 @@ module Svc { ) \ opcode 1 - @ Enable / disable telemetry of a group on a specific port + @ Enable / disable telemetry of a group on a section + async command ENABLE_SECTION( + section: FwIndexType @< section grouping to configure + enable: Fw.Enabled @< Active Sending Group + ) \ + opcode 2 + + @ Enable / disable telemetry of a group on a section async command ENABLE_GROUP( section: FwIndexType @< section grouping to configure tlmGroup: FwChanIdType @< Group Level enable: Fw.Enabled @< Active Sending Group ) \ - opcode 2 + opcode 3 - @ Force telemetering a group on a port, even if disabled - async command FORCE_SECTION( + @ Force telemetering a group on a section, even if disabled + async command FORCE_GROUP( section: FwIndexType @< section grouping + tlmGroup: FwChanIdType @< Group Level enable: Fw.Enabled @< Active Sending Group ) \ - opcode 3 + opcode 4 @ Set Min and Max Deltas between successive packets async command SET_GROUP_DELTAS( @@ -102,7 +110,7 @@ module Svc { minDelta: U32 maxDelta: U32 ) \ - opcode 4 + opcode 5 diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 875dc271ce4..dd810b1b52e 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -95,18 +95,29 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { U32 id /*!< The packet ID*/ ) override; + //! Handler implementation for command ENABLE_SECTION + void ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType section, //!< Section to configure + Fw::Enabled enable //!< Active Sending Group + ) override; + //! Handler implementation for command ENABLE_GROUP + //! + //! Enable / disable telemetry of a group on a section void ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Section to configure + FwIndexType section, //!< section grouping to configure FwChanIdType tlmGroup, //!< Group Level Fw::Enabled enable //!< Active Sending Group ) override; + //! Handler implementation for command FORCE_GROUP - void FORCE_SECTION_cmdHandler(FwOpcodeType opCode, //!< The opcode + void FORCE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode U32 cmdSeq, //!< The command sequence number FwIndexType section, //!< Section to configure + FwChanIdType tlmGroup, //!< Group Level Fw::Enabled enable //!< Active Sending Group ) override; @@ -179,13 +190,13 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { struct GroupConfig { Fw::Enabled enabled = Fw::Enabled::ENABLED; + Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; U32 min = 0; //!< Default for Backwards Compatible Behavior U32 max = 0; //!< Default for Backwards Compatible Behavior } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; - Fw::Enabled m_forceEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; struct PktSendCounters { U32 prevSentCounter = 0xFFFFFFFF; diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index cb0c7c89c49..845c254425c 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -956,27 +956,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->m_primaryTestLock = false; Fw::Time time; Fw::TlmBuffer buffer; - this->sendCmd_SET_LEVEL(0, 0, 0); - this->component.doDispatch(); - - // 1st Channel (Pkt 1, 2, 4) - ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(0xFFFF))); - this->invoke_to_TlmRecv(0, 10, time, buffer); - - // T = -1 (Check to see that setting level 0 does not emit packets) - this->invoke_to_Run(0, 0); - this->component.doDispatch(); - - ASSERT_FROM_PORT_HISTORY_SIZE(0); - ASSERT_from_PktSend_SIZE(0); - - this->clearHistory(); - // time.set(100, 1000); - - // Arguments: opCode, cmdSeq, section, tlmGroup, minDelta, maxDelta - - // Set level to 3 to enable all levels + // Set level to high to enable all levels this->sendCmd_SET_LEVEL(0, 0, 10); this->component.doDispatch(); @@ -996,8 +977,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 1, 2, Fw::Enabled::ENABLED); // SILENCED will not emit packet even while enabled - this->component.doDispatch(); this->clearHistory(); @@ -1007,13 +986,11 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); this->component.doDispatch(); - this->sendCmd_ENABLE_GROUP(0, 0, 0, 3, Fw::Enabled::DISABLED); - this->component.doDispatch(); - this->sendCmd_FORCE_SECTION(0, 0, 0, Fw::Enabled::ENABLED); // Force telemetry on section 0 for all groups, regardless if disabled + this->sendCmd_ENABLE_SECTION(0, 0, 2, Fw::Enabled::ENABLED); // Disable telemetry on section 2 this->component.doDispatch(); - // Disable output on section 2 + // Disable output on section 2 via port invocation this->invoke_to_controlIn(0, 2, Fw::Enabled::DISABLED); this->component.doDispatch(); @@ -1021,12 +998,12 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { /* Configuration: - Port 0 Gorup 1: 3, 15 MIN 3 - Port 1 Group 1: 2, 14 MIN 2 - Port 0 Group 2: 1, 4, 13, 16. MIN 4, MAX 12 - Port 1 Group 3: 0, 7, 12, 18. MIN 0, MAX 7 - Port 0 Group 3: 6, 18. MAX 6 - Port 1 group 2 Ignored + Section 0 Gorup 1: 3, 15 MIN 3 + Section 1 Group 1: 2, 14 MIN 2 + Section 0 Group 2: 1, 4, 13, 16. MIN 4, MAX 12 + Section 1 Group 3: 0, 7, 12, 18. MIN 0, MAX 7 + Section 0 Group 3: 6, 18. MAX 6 + Section 1 group 2 Ignored */ /* @@ -1044,23 +1021,23 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { T=12 Tests updating packets 1, 2, and 4. Packet 4 on is emitted since it is updated after MIN and before MAX. Packets 1 and 2 are updated after MIN and may also be at MAX, which is then emitted. - Packet Updates 1,2,4 1,2,3 1,2,4 - V V V - T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 - - (Bass Clef) -|-------------------------------|-------------------------------|-------------------------------|- - Port 0 Group 1 ● ● | | ● - Port 1 Group 1 -●---------------●---------------|-------------------------------|-------------------------------●- - Port 0 Group 2 ● ● ● | ● - Port 1 Group 3 -●-------------------------------|-----------------------●-------|-------------------------------●- - Port 0 Group 3 ● | ● | ● - Port 1 Group 2 -|-------------------------------|-------------------------------|-------------------------------|- - | | | | - -|-------------------------------|-------------------------------|-------------------------------|- - | - Note: Packets 2 and 3 are updated and have their own independent counters! + Packet Updates 1,2,4 1,2,3 1,2,4 + V V V + T=0 T=1 T=2 T=3 T=4 T=5 T=6 T=7 T=8 T=9 T=10 T=11 T=12 + + (Bass Clef) -|-------------------------------|-------------------------------|-------------------------------|- + Section 0 Group 1 ● ● | | ● + Section 1 Group 1 -●---------------●---------------|-------------------------------|-------------------------------●- + Section 0 Group 2 ● ● ● | ● + Section 1 Group 3 -●-------------------------------|-----------------------●-------|-------------------------------●- + Section 0 Group 3 ● | ● | ● + Section 1 Group 2 -|-------------------------------|-------------------------------|-------------------------------|- + | | | | + -|-------------------------------|-------------------------------|-------------------------------|- + | + Note: Packets 2 and 3 are updated and have their own independent counters! - Expected Output: 5 1 1 1 1 0 1 1 0 0 0 0 5 + Expected Output: 5 1 1 1 1 0 1 1 0 0 0 0 5 */ @@ -1107,6 +1084,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(5); ASSERT_from_PktSend_SIZE(5); + + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 1); + ASSERT_EQ(this->m_portOutInvokes[1][1], 1); + ASSERT_EQ(this->m_portOutInvokes[0][2], 1); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 1); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); // construct the packet buffers and make sure they are correct @@ -1175,6 +1160,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 1); + ASSERT_EQ(this->m_portOutInvokes[1][1], 1); + ASSERT_EQ(this->m_portOutInvokes[0][2], 2); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 1); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 3 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1192,6 +1185,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 1); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 2); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 1); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); // Pkt 1 comBuff.resetSer(); @@ -1215,6 +1216,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 2); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 2); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 1); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 1 on Port 0 // comBuff unchanged since this->m_testTime is the same ASSERT_from_PktSend(0, comBuff, static_cast(0)); @@ -1228,6 +1237,15 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 2); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 3); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 1); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 2 on Port 0 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1258,6 +1276,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 2); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 3); + ASSERT_EQ(this->m_portOutInvokes[1][3], 1); + ASSERT_EQ(this->m_portOutInvokes[0][3], 2); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 4 on Port 1 (Unchanged since T = 0) comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1278,6 +1304,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 2); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 3); + ASSERT_EQ(this->m_portOutInvokes[1][3], 2); + ASSERT_EQ(this->m_portOutInvokes[0][3], 2); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 4 on Port 0 (Unchanged since T = 0) ASSERT_from_PktSend(0, comBuff, static_cast(0)); @@ -1290,6 +1324,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(0); ASSERT_from_PktSend_SIZE(0); + + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 2); + ASSERT_EQ(this->m_portOutInvokes[1][1], 2); + ASSERT_EQ(this->m_portOutInvokes[0][2], 3); + ASSERT_EQ(this->m_portOutInvokes[1][3], 2); + ASSERT_EQ(this->m_portOutInvokes[0][3], 2); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); } buffer.resetSer(); @@ -1305,6 +1347,14 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(5); ASSERT_from_PktSend_SIZE(5); + // Packet Location Indices (Checking proper Section, Group) + ASSERT_EQ(this->m_portOutInvokes[0][1], 3); + ASSERT_EQ(this->m_portOutInvokes[1][1], 3); + ASSERT_EQ(this->m_portOutInvokes[0][2], 4); + ASSERT_EQ(this->m_portOutInvokes[1][3], 3); + ASSERT_EQ(this->m_portOutInvokes[0][3], 3); + ASSERT_EQ(this->m_portOutInvokes[1][2], 0); + // Pkt 1 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1344,11 +1394,105 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_from_PktSend(4, comBuff, static_cast(0)); // Port 1 } +//! Configure telemetry enable logic +//! +void TlmPacketizerTester ::advancedControlGroupTests(void) { + this->component.setPacketList(packetList2, ignore, 4); + this->m_primaryTestLock = false; + Fw::Time time; + Fw::TlmBuffer buffer; + + // ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); + // this->invoke_to_TlmRecv(0, 10, time, buffer); + + this->sendCmd_SET_LEVEL(0, 0, 1); + this->component.doDispatch(); + + // Send a packet every time the port is invoked. + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 0); + this->component.doDispatch(); + + this->clearHistory(); + + // Expect Packet Default Enabled + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + this->clearHistory(); + + // Disable this group on section 0 (primary) + this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::DISABLED); + this->component.doDispatch(); + // Expect No Packets + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); + + // Enable group on section, but disable section + this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::ENABLED); + this->component.doDispatch(); + this->sendCmd_ENABLE_SECTION(0, 0, 0, Fw::Enabled::DISABLED); + this->component.doDispatch(); + // Expect No Packets + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); + + // Enable Section by Port Invocation + this->sendCmd_ENABLE_SECTION(0, 0, 0, Fw::Enabled::ENABLED); + this->component.doDispatch(); + this->invoke_to_controlIn(0, 0, Fw::Enabled::ENABLED); + this->component.doDispatch(); + // Expect A Packet + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + this->clearHistory(); + + // Disable section by port invocation, but Send Command Force Section + this->invoke_to_controlIn(0, 0, Fw::Enabled::DISABLED); + this->component.doDispatch(); + this->sendCmd_FORCE_GROUP(0, 0, 0, 1, Fw::Enabled::ENABLED); + this->component.doDispatch(); + // Expect A Packet + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + this->clearHistory(); + + // Disable group, but keep force group command active + this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::DISABLED); + this->component.doDispatch(); + // Expect A Packet + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_from_PktSend_SIZE(1); + this->clearHistory(); + + // Disable Force Group, with Group Disabled and Section Disabled + this->sendCmd_FORCE_GROUP(0, 0, 0, 1, Fw::Enabled::DISABLED); + this->component.doDispatch(); + // Expect No Packets + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(0); + ASSERT_from_PktSend_SIZE(0); + this->clearHistory(); +} + // ---------------------------------------------------------------------- // Handlers for typed from ports // ---------------------------------------------------------------------- void TlmPacketizerTester ::from_PktSend_handler(const FwIndexType portNum, Fw::ComBuffer& data, U32 context) { + this->m_portOutInvokes[portNum / (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)][portNum % (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)]++; if (this->m_primaryTestLock && portNum > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP * 1) { return; } @@ -1427,4 +1571,12 @@ void TlmPacketizerTester ::initComponents() { this->component.init(QUEUE_DEPTH, INSTANCE); } +void TlmPacketizerTester ::resetCounter(void) { + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { + for (FwChanIdType group = 0; group < MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + this->m_portOutInvokes[section][group] = 0; + }; + } +}; + } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp index 2899c2dfc07..0844f0fca0f 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp @@ -13,6 +13,7 @@ #include "Svc/TlmPacketizer/TlmPacketizer.hpp" #include "TlmPacketizerGTestBase.hpp" +#include namespace Svc { @@ -83,6 +84,10 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { //! void configuredTelemetryGroupsTests(void); + //! Configure telemetry enable logic + //! + void advancedControlGroupTests(void); + private: // ---------------------------------------------------------------------- // Handlers for typed from ports @@ -119,6 +124,10 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { //! Initialize components //! void initComponents(void); + + //! Reset Counter + //! + void resetCounter(void); private: // ---------------------------------------------------------------------- @@ -132,7 +141,7 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { Fw::Time m_testTime; //!< store test time for packets bool m_primaryTestLock{true}; //! Lock limited to entries from port 0 PktSend - + U8 m_portOutInvokes[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; // Svc::Queue m_portCalls{}; }; diff --git a/Svc/TlmPacketizer/test/ut/main.cpp b/Svc/TlmPacketizer/test/ut/main.cpp index c5b49248efb..c082847ed0a 100644 --- a/Svc/TlmPacketizer/test/ut/main.cpp +++ b/Svc/TlmPacketizer/test/ut/main.cpp @@ -79,6 +79,11 @@ TEST(TestNominal, configuredTelemetryGroupsTests) { Svc::TlmPacketizerTester tester; tester.configuredTelemetryGroupsTests(); } +TEST(TestNominal, advancedControlGroupTests) { + TEST_CASE(100.1.10, "Control enable sections and groups"); + Svc::TlmPacketizerTester tester; + tester.advancedControlGroupTests(); +} int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From 7f0a66ac9bcf1576f1b6c08ff13c5d4f5f403042 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Wed, 28 Jan 2026 15:44:03 -0600 Subject: [PATCH 10/16] Add SDD --- Svc/TlmPacketizer/TlmPacketizer.cpp | 8 ++-- Svc/TlmPacketizer/TlmPacketizer.fpp | 2 +- .../docs/img/TlmPacketizerBDD.jpg | Bin 107137 -> 99889 bytes Svc/TlmPacketizer/docs/sdd.md | 36 ++++++++++++++++-- .../test/ut/TlmPacketizerTester.cpp | 4 +- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index d7de883c724..a98981694d5 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -522,7 +522,8 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if ((0 <= section and section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) or + tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } @@ -550,9 +551,8 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (section > NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or - tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP or - minDelta > maxDelta) { + if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or + tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 3fd44043148..ce6aeeb74df 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -10,7 +10,7 @@ module Svc { SILENCED, EVERY_MAX, ON_CHANGE_MIN, - ON_CHANGE_MIN_AND_EVERY_MAX, + ON_CHANGE_MIN_OR_EVERY_MAX, } diff --git a/Svc/TlmPacketizer/docs/img/TlmPacketizerBDD.jpg b/Svc/TlmPacketizer/docs/img/TlmPacketizerBDD.jpg index e29e86afc214f3a8e21dae84546b0c714982b508..f3a1ef7650cb98e526613dc6f466b11454e23cad 100644 GIT binary patch literal 99889 zcmeFZ2~<=0vNsw9ClDjEfRI)}1O!@TP(ad%%ncC`P!Q5KA_T;UfJ6vM3o=hFW6Km} zFfv9ZfHEX91RPqCS%MG}S{WnRDq%~ew>{^cbNifozWdJlzW3I8YrRWWSg@1*&#v0_ zulm)msv>zSnSt$ews*3JZP>5@_7n7gN!YNxb~wyM7|hibrVoR`wnDEdz@(tRHb9TR z{75EYw%^Y9Kl=H$$p6s--%gf1g~@N-__I{9)P}vVjq)3$72H~n?(zg{r( z|3)e4O`ErD-6kUoy`gz0Y~uzgsg2T7n>I;HL$Ag|ufwF}H!18nWV2b({rr}_5lV+| z+`7AU--(A^${vF}?IS;5P1q)*qPj~>U1z`U0X=;aQ#13U$1H45+SxleIyrlu_VV^Q zbJjQTLeRzF5KL%fRP?nN-1XSRn@PzjscGrCdHDr}MZeuHuBfc4zDK@aQ~Rii+T7Cm z_(@xLPcNgd|Jm~wL&GDZW8-fpm~77MAMZcR%`YqhpFV$C<*$KX*T2cN0Vef#mHkz+ zf09ccl53;1w3PIgZ*pze81qeVdFf4i4sBMjao=)2LUHfm8(Wo5+`9X)Yummf9z5lr zuMWznXq&KgfN!GxMY4ZSu!R3D$^I_bf6B##ZI{{r-8?CI7!oF)wLXT&q|B73vE}UW za1JZAZ6LcNu2N{h>%S>fHBL5MEwe?BQD0$wekq}-f+;=f$_=Gv)(Cp;^~PqL-<9;E zxemW>_(0+fqSY+Ly+rB(3G7J|KMYxDZ6SeK5iO9Rq_1$ob2R@NZLH#& zftA^mr~X^`KevN$>oVOm@m|6RO$rRJ4e@16->$}J>Ah)|z;=Ky7Q>~y$D`#C{zX3w zH%7~uyFKn0r8bz&ooP)N9Y!U5z}VG)^=eSp&&$O!IaeNsOjZGB(s8lj5?Hib{kh_H zch{=vWNOIkrkY*8UwB}>iz}1YeZH0xsw0|{@o;y0{=ACm6E;O;SI`a<2XBer&_QqU zE22qL91Bg%+q_j1H@S3KP!!M%(s$4}j zDV1sV5Wp^@Wvw*B1s1O4Ghi#4N6zU_Fm5(hJGYp(WD!-aJk%&uyQ@)nIP03f!q)e{ z;*o4PXh7uN;LzXh^UdqB)*P}z3x2?D;eR(Ofnkg@k-Iy*I4C%11-gY3B(T0>?>2>6^ko4dbY3G zq7H%aEdTX==A(Bfb&ehFCl%^__E(gNEb~2m{d$mx*B(<#yTK@O;?06H^w`5#P1{11 zNK9*(0g29nZtpG$Z2p-97J?NTATQFz1S+w|PIzHJcw3b#w<>|%gl>BO5-@2xLsFz2 z6rQ7t(g;$x0b4Mq0kq_(_BOciBZsYQW?66yK{@YSmeqAm&XzN{ia1%sP8`vnH`Up9 zH@IfHTkVTpMT=sSpY8TE4F?|O7i8E#4gmq)oy}20GT=-fPH+lM1Aj_XwM;k&+Hz95 zBrsW_h3GfoAs(4sJ&<)tXnY?;&QA4rqc+Ts6ArirI`Z!29VMn~9n|u!q_{>`kRF(f zS>7jhe0Do@#uuzVesQE^uKWCh$2BkH)AOshc`{BM3gtRkU#P6(RHkR|ap#{Ztq#cE z(RtL;?!{5&+ghc{Ya^#ec+uzb!ltfZT9`RY=0X#nKGgbQBvVxalNp$xNMNL9#J4Sh z{0Ega^N3woP~V4v$|7tZkBW!$qu)?TOm##~Y|K-Z9H_`37=cAI=#;glMWSpV*cHx+ zk6~C_g9dCO0%Ku|bGCf@Qy4sn`C>7);w>%5?C#xwtjpFfyCtwz1VeygwjpoO<%Np( zz(9ujS{EvXX5Ps=-k*NSh_)Nl7q|kdy-Voy7^X4fr7=7MtFTzaM5lbIqin+kdiw7> zGZG~@wkl`q$ot()UD#q2RqK&s>iAgC0q&vK{OZ8ZS4M!-q4xTpu0-kES5=33I(>dj z9a4jMDFpCLv^rGmTotV9u6)Ic+>__+STS<^y;{x2w($ zz-BEAH375!*@2v9YzjeoG}?GKj>JYKQ}f@PAN>LvHxpIxhY?CbL}b;#!D(dxo_6^= zl}{evEeUK>1W1~(gm1-#4)8rj@%}*Z=U$Ss2}>P3@^lTULeK$YoCd9TgfY#NI(%wz zalJ?xnhb8mr|t2mX~WgQ4kN1|c8+w&qlL+8K;;R0lG}FOO~EVBrf#fE{kQa}(I<-@ z)yv20?@(6Sh-WHaSJs~%KkQ_wjEr`R4t4%M$zp`Jy1V>Aop*knXJ$p`AGIDC1;_jf zy{UfT`@W#Q>1Th01h(Bla6$t6=4fUmFrS8R_VaDeJx7LkE=gd@kmmZu!RXIleGmd; zaJFB33|QkHss!`;1I$LFTwIAy*9UG;S5z=sDnmjW9_vSd)$6MtlHtS~`}KmW zf>?}h(oNet*6gSww`q0tt!pJ4sexd|u$3h+E}7#(cNXk=FTv&S`Pa&sO!XA zV;F06HBRBLjIhT)c5LGXaw_mfKx1{}ok_e=TT=^QR2dN+SdVkCP4Q`aKu@E5JNt)_ z@4`r%!Q+BUc-&BQj=a$S379Bw5PI-PNzrJg_X=n2Chf3rA8(}_J2Bift;Tam6=JLL zM|0RQeFD-=+J;Bt(Z@`)bi2#79`6J{b4+{3T4P5ZVTSzuZgWktsg-EQH)XrMLPm(8 zN1fWdJnm7v#e>dzj}J^MbmWeNI~nd9@LW>jy(m==AFWZf)MHIpJJ!9>uPkYMpP4gG z0z)X7q%RkURAaF+y$LOWAw!W+tU4rt4dxQwREj?gOf*dn&(^F`lEvF4ur4>+_xNa@ zv@bT17;L-)!=gry_Zuq+Ra(KAS&BN|8H||GxQ;jGeHh@BNnokbSVi2@%)J|cflKQX z9;5dSrqC0=CoW^kD?IZ;$hiBGGMP=8T;l}f2%PaQpcjDXGOi9SR_&ssAe2o}QZx!C zj!ltXSiejHWU%Si#5(|S#$A73;Yq;gu5}U_#U9x16oIKlRX>fEJ2B7`(N)(U7L6I{ z=`U`}GAYoDd$pg9hHv*jc7KZIkWyT8EKD7P4B-qrQcF))sySJNR=8*6KMBw|e{g8* z2>(^r3GY&7hte&6&Of=_dLlBrBIld3;*soBC`_S+2E>aLF=6y;1bYC~SLNCQv)wfK zs900{`YK-EVxW(8;~Mia1<*od(zd>_Lh~{>-fers;=)lJhh+h~rG(LhSqW*Y*HU~4 zn5P(?Z!4U$2g<;7J)QO7Zeoi$*SvO-C~ZQQ#_NFo0K5mO5KU5Ja5P;QM0LE~J)t+R z@5YP-hQPJj0;u%M9foZ5FIT|>VA#oFYMkBR_9m6{t7sXb+vLsyu6S{c&BNPjhOPlHS!ZA9QSTaKYCn!Q zUCs50$jBPIzux{2t~U6uuJ$iV`I`li{%S!&9&cQz%<8AudRkPf*>GOmbWZ3-S|55N|3S&Ilv$FPyrhFN%8;iI?R_dbOA z-OFVfKeDo&#pd{u`i9u#8OXayHG5&k^B0!Nr* z1Q_#nlySCwxoM1zxo(z527OMMmxohh9!;Knt>S_tHYVmc@0}!oi%ei&-jMlcV2gzD9@6>2bF>VV5_lZx{FUTkOUHG`}9ywS=FOO}U7j}jeWJ1jTzXvRzfHY=U> zJuaBDeO<0XXal@%5FnR|TT?pZ>ikMHEXz>tk3kF;E5&kbt2f@-iK-AnsCg3_*q;T= zxr2rPa_09_^A_+P<238ohz;9k-rc7clAKW`g zC1wVE2kcL@ax6C(jIz73+N|%TKS(}%pxQgh%GKhs3)4Bt%UtHxcwx*bpG?2fb8Iyo zJ#BWo6T{vkQ*X4)x3tk@Ax^JbA@g1R=+`He+NHY#_4Ll36U=?vDryo~$v)9tIKX zwg*EU{#fD+S}u$xHB!HNChB&FYYlV3Hznzd-&QgWwdv4^S<}N(`?b!iM@an*e=vV#?Rk}>XDJ*$z=7WjP&sSci~kmQ_ir9 zOKrhE=c<|!e8+<%x43{(w*G^P_imWSBbmWR$p?<@FB`@j;nKkn(l@IiQFXsXonR>H z_@e$CiT>tC7C;+8gm_E=zn=;q(}c!6QZG`GI`E6N6j;`{P<{}6(ul*d?^OqJ(7VBv z9^Mp2m7pXdHLs|<&c1RxuIkv-!ka2gyMHFsr|8rI^W33n2Y!}qEwCn363iC$Ak~CQ z;CT*u0`5B^-j5@7(Y6C~eJgN05~(KEULeX*5t_K-*`7An))c(!4CRK!>x`N(m7n|8 z+(!e!ThY8am)4f7pIb&Rf3)j8b)l-v!b`EO!OrJi;iD@Nb%ylY_E}C;+AhXayV<$C zgwm?wOoNd-e~hI^RqwJ~bhQy3ORnk03SmB;R%<)az7w+17zq5~;es>bNi75zGQAII&4Kd5<509EK4LMT z7#^?-i0RWr@IA(ahsCD63Y>4etPiM*=qfW^CBl7~zF>lQJ=}cuTGoBn`%IgAI-Qm0 zd_!yYoPCkyI8>@$%C>hu_Xa=Idz$Q4pc5>_wADJ7ID5OA#_2lgCfB5Ux)qrFkqVq$ z?US}~^hXB$Cc2g0ott#`h@fT9aV0RJ3A)c3IlB!R5)eyd_z6r!#QFAEVtL44AYK1I zCD0PsY7FJoN{01O(LM2wsSu*nGCho#VQoa>jJ7VXQ6}&{<5s3TSDHQ^ave8^InxC* z#9f%!KxjFcFZ7hpAxwVGH41yMQHUIAHZWN7zv?}*;@(j4NU+97kJGdltS!MMo-Xvc zN%}?(`W~Wg!{<27x72>tTkFkSvJrH+@poywcc!8%~Cz(SF`-lgta|n zl4>B*vV7^1aet21(N^Q$U}aevboK=G?$#RdDx1r_tL6-)iAs5$4qKmFdPYX%=IHG9 z*Z)!wV*YGeIc^YBo#`Is-_Y=N{IE}cY*q6fV4OA?3=;%&b3XntIWhU_Kmv1HaM8tQl`(rzF5vXBtR4Re|3hX({15p5r`+)` z+m6;+@yq|b|9|uYaS8up58v3yZ~5~7Kle+cWf!6anPa_o!DpRylNg9%YIqJu4JhH- zyIPC3^rD-ZNd+8)6fVz`SCm$duN%nXInScTsqrVCovScy9=|omd}-Qo%UJ@8@|o>V zS-uCDC(vYtM<0({c~yXD4g}Fqd^&F~bCwsAfp?mT5A+^1Vaba}#Kz6T(KltX^27byv1@jE53J8N9H z1eSJQ@FPUijFX8=>#G~r{#|(;#Jd7#Qbb$mAw;1IoC(3p@=gS=2O*7|w-ne)V4O3A zVWe<%{(oyl^jAx0Nis!P5Q}6dTW=-4iQ%V6V7(7S_lRGY28o}>is{BJETf&Hg~HUFgG)h_&LQ8rBxXUit`5>-O9bVPR=qFLK6s;zV09+e7RC!pwH*Kh-2dOSTfHxXi{%94+yM=e)blRS3 z;cdyZG9a`37u(*b=CML=y&KR_$yWP0DKmJAi@ZRYNES_7BGM4MRsu{8xA_t#NL168 zRiR$K4rd78KRLbqMfEco*#o-T?qgGN(Q?hP2262$CqDb-{|(OmmW})cN8CZ4C*4?< zGNKD?mA~8Rzzr1q_^Zvl4a6{065u}{-FNFu8)-RSU>1^Fr4AJk%rJ^C$}e15B(@lH zmCql%HK#FTNy!+ryoY;trxEuW;~$NQT^L#+y!81)gG)gd5gD4Of2F5Ck@|l#o6*}_ zmXW;uA9j5->~Ip;oU=K6@YjJYT#UfQW&8aeec(BQk?df}RZr*)~h zaYT9X8_%Bargpy(kCCVy4;|^#PKunKeeh^)&;6oQ@y5xHh<7;BtWd%EQHT30fgfS$ zp;ekp^6g3=@KODe!mmS*683q>?%ZlJ*|Huhf%#kT<@l!be=Ltdu8<`=`nVJEyD!aOy(LN^w_t(u75B7%!TOl&w{k&pPA|kzwhsYukR4Qk-)%0 z1a&|HJ05E+BBFm){kK0ZZs2Xv%>J@)!w+{kq?{JBUyT+TDaf!KrpFjl`I$y_IWa>| zDJ*xh|1Zy_j_rK}RRu4gCdxmKEv}DqRQobYG(Kvwhy~w|)gu591Z~6@%1B?J6eig$X_i52Wg5z<)Kew-3u9v_r zVVU|Ln6EY#8b@0pRLc#bWn@(fe%-XG=@(a|izr(HyC{mNc2(^sYBZ<(V&GPc=B5ek z7Ug7~>{0ikf}Mvl4@v7?k=psg^UWCzZJh*v39PXweiyL>Z5l@LijMx!e%C5ofX1z{ z2X5kBW?AZlJ_(GfmBvPFnP;Y?b~x1|q5Be?@qXCEUw`W3s>X+=VVc<#Ys#>p{_2lE zHMKl`lE24sPwt7Gq9&}xyUDJrzubJBtoO#ud{cpnom_(b59do2j=U)r;=xyJKVs6f ztOS;jh1cbMpwBkAJY}MmBWEb;m{@c+Z4YjeV>d?PAXK^lfeWx{s<3I68`m5AifWNn zbo0yBKOQEF4(*(I^lil1(7q2_gdWg zop>+pp+JW$T!*qQ)^Cta7vDZ{eZAV|rb-NjMSLwJ_`MFx3?e@4iHk4WFi=h(Crtl_^e$C#2id`%?l8ppif-7+v zy;|CUcT(K~8qsmK3iruBrY{rG=d%vQxH98@m2xEDg6j#Tq~8UM(@f7;IjC3p{_gXz z_Wt{3`Y&kS$*j1i;+RE!EriJ~j3c?%f!@RXg+U`Y9|EY60bB1{v zBF98z+gH3jrf587%uLI$ZLDIL@*!+#12!fth0B^SM9wi;Vj+}k_YU%1P+$425*Qt# z76Fr^_Rh}k9-i)=uKrH;rstJbqK1-U2c4#Lyepc?2DeIQDk}BtY}~$gc0JG+N%p{K zFXuxwv}4B27FY7x(}Ulq^dr(p57&AT30k|r<2(w#6NK}cJGN1TrehtdHH&Sp%Vrx4 zoKo17G!xet*J0x*A0Q>gc-7LcBwpEMuAsvf8>#-1UsxDpySqG zvqfD7Bq^aCO3i7uY%h!RqjyA@4}E9j3RI(?nw;9-xxb;xtf6KxX6=C{(la^S$m!RG z;YsRF3qq18cUTIyr7e*9(Mo3_aUVQ!GYxxFab*c4L`K3!<)hDu? zoL!HZTHs>cuV8Oo-Jg3k4`-is^mNi0JNu~lp}VM}sZ707Wxn~9yXx#LA}MG4yg!`d z>IMSijy!}=0dC6+P8Y)O3D5BkdP8)P*^OD(xZcC!Vbaczw7upT_(OMmK_5H7>^5*> ziF1%8Z=!j1xX0`Y7{BVO*vpK6KbAeN9{p~Z=|5#>jJ^kz$)yX24t@!9`Q`wmZRh22 zt|nZ!S>tk z(yVv#C^uMrXhn!?bpl_8RpMg0XgZ``n%cwRgVFG;X*IwM0wVFm`%^eG;#S;bcWb%A z$VF=BU}M8LD&d_S@8wLtP~r0l@W>1{8LfhZXk3y4-hr(d$q?`F1U}vXau}=~rv6w@ z;N!h$lp+|KEQZx_Uq);do`AEM@OTdcsN-!=H4AJ zEBG*~UDIDIyw#SK7yQBDDJzThXuO-KA-oJ^KM@3q2d4L>aLDOjTv0u3Mv2!ean1u> z0V=$KWXJ;LLIQT@Kj*AKpd1_EH}Tfs-IDwMEV-A@RF3Z*^^}_c^JX1{iaCr2!c&R8 z2n}l)Ru53(GE7CM$q;mHw+OXkFW(A|IDbBTPs`ZdunkyDjMWL(?&yRHLZ#%%Ue}C<3)gM#us&(tD_d~z0O@@E`(MRV=R0MvorL=Kv&1b@y z?{rqD;*F5;1ke>Ky#8$0!CUQTpc6bWs$x}OAknA+$qxIy0xc(W1TJVD1n~%Q#`Ma**&3N5SZqVjoo}28*j@g5-J9IfQLB9iS?CPR2K#G(mL1-9KYAhZpWs- z!dL-dkIzUnm*CqRvQ8VVwZ|Tyf)v9j)$03$MkSomrhvI_Gv;b} z=~__}F74y0TC^-y373vgdU@Y9Qa5wlcZAM12oL5)Vnv?cBGP~EPV46on`}KMfvtT& ziuX6X4nOeXYOY81aX*4+t9_YT7{BJkp(OK;z4?I`>gA>m(33pNl-l&YwBLIabGJQz z;$|?F7fzk{x`i!)ZG(!p*0W_3kIFi24w#!~$I&!fnkA>2!grkMifQn}rHffz z<*GCwt3QRN#at}r*88?JgwOaN=^0yZSSyLV?+~f|nOfH0JCIFpQq8293Z=kyHg?i> zMmK2&L`HA~#GSZEPuh^x$>~Ep2`u9adO4#L={8yc5K{4n?oWn0&5Y_i7-x6?Msf>h zkAE3hE+qXg{V;yWx%4#2P`lF29Dgel8*-KVyzv1T@Ji96z@pYU0#$ywetIZcdsgrC z2XDgQW1A+;Q4us;N=@AsY2tcEcvLnJ8st>gkNgS z&}<=ZkCC=DxRcJ^pYp-*{YO}i+G9XfsL;1`8fa`nGEfPq8oce3FuLkHsNVXv%#Cto zTIUfE=lG1aSDrc7>lnuOEu zrcCZE>fa*izn{wrax)Dy@LjB}c0G-9y6;fTw-Z$xH*2|huZB&$xJnKl&h^rFRsqj_ z7%hc*9FF7RK%#hCJ8!m6tP5t%u(FVwOo(c@=h2%0tp{{PPFuWVXj^+&E4u+3T~~6z zO60|y7Zp!y-u&vmd~<7m7Bhs%a}=O5EXaK=;9XwjDPZj3_1Q21iA&G1))~j^NV60U zo*%|vD@ONl1N(|-QiEK)Pq4Im*fn3DluOGishEpN)iKvZm;XD%tT8JyD{(70FnDhm z)17~i?FPqn8y$*BA3S?DBq<2f_hMA@Lz`>!#XnjK#>%k@xnr+ zlC{#(mFZC4;+$BKl2d*FOw%cz#P~L*h22^3#4YvFzAJL%g*V=ASZo=iZ5`Gc?)H|z z!VAwtT`0Mf5rZ10&*Z72vxWNr^?nY$`{T6h^FBQ^SJAmE@NwdTUF+3|p8T@J9SUY)tT0NME9&9)$bb;Jd8WWpp_=T(M z1z0ynkTDNKodPcuIbpF_&F1F%?^k0B(;y#PGDZfm*BkI*ynyEWLc``q!$M@;5`wzY zYv}1a_NDWPk3Z36P^=xM%G&wS6q%uJRMbG>HPgttI}v$V8i>Ph>n*3sun1|L^rf_npYitZtQLT?OF z?@zYU13u3*=G|Rlj;G;mYv1+*8g2zf(L~iyb6-lT5!8cF`lyF55G>sm{-&KR8)@|N z%-t+6)jufPgir%+f(_2x9w4Sr2QmZXN20K5ps^o#J)R6U@oPcSY*@T(7ft(Z^efq? zE}WILSeDQr&ehQ-#K`ZdLr;@q8=;tXdCK~Ytw86W(axW6u!8edD2C*vCte+WY{$6Y zvX&l^;S#R;Q^u8)ro;&Hb!*4=6Q46ON%}*dG9vPtnc>GLz}es#V^G_R{EL+*k1aL> zBRvL%f$?lhQEn>oatU=v3*Yc=ZOBkC}9GOW|bqb&v1^k|6sK6!}A+$w>cB>Y3H%=cv`Y;E~v z=hdyr-`lK88`;#BlHFb1A_+tKiug}%r+glLa9j6(%#Xdbt547VPSG}6*mT|UF=ToQIE*nqr8iuo$KvnJo<5(+cI5`@m+7dHrbB#UKtN+ObaQ@ z_@N?QKUvA$4)*`XQ~4{LwSS+om+SX(_b*(L_q3vPr z1&hP|Wx08KD72-^dwUww6FUR{?0%KUKf*;&&18~MVI(aWxFe6g@QFL3pBh;*w zCI=+-M&MrdL@vZ;78NJoy3U@mf3eu_bvWRRi}Kux0Yj`P9wMoKVQm6idZ5I1XGbt^ zjYCYO?G`hbDrGS<#1y)m>MS~=!;=HOhvnVI8zJa|iE!bbX-&X9^}E4aRe%Gdy&<@J zS^>?~$<^2sqdh3j21^)3o`VFYj*6;jBn-;tm#)e2DhvEY95-Ym=Bb%$Q$Zt)B8v7wGj z#oC1Lhw=WZ2_3e9oP6H99P^aCUX|kJf)VxZ`K!;hiyi6NGnkcq&3Dp%%S%XHqek3( zaJ1C?VYT_L6V<`ar-ASDe7!PjCfd=tZhB;cg3OZMkSMkmt;<8L>I|&fS=+MX74nh^~3}OgTi!hLjv7m6n%sK%-uauencLDQ& zG3(7>j_pu~Jded#rnV$9qB_q@k{YKaXrl214#=9jjDKN!`4iFj0Ex7FP$o?4d?gwu) zde!-r`>V;Aak0{%2}wR=-L+sjmx!^ed;7wWy~fJ1-T^tqv-rzAcoIPwL!<_Gtt1e4 z;Pp7$@SfNi(oGA8<@e$2SGK)!WSgqV&@~I69GU{SKHxeU#XY>?Jb6h=J>|rhVYolr z)i(5pz3Zr_64)-{3`F<92?%(yH3B3FEQ#IwB4!$7*&kcUi@x-fXa^uT$;z^-C9QfP z!00h88`FW>ZV}b0@;f!YvC{A~!4?AyP5oyXs`LhW=ridU_~ALsoZvFN*dd|ch^ zPFm$c;d!h{&FXRcaEm|S7O!2F`de|#4<6GuS&|;bP=|tnwuCRvTR2MfGFVSfIgu6XL#e%ADr=E9yqMr|Py_YZaV*u4mui>>g3( zvjs@UAaYukLF;txDpfWcoFB!;5lh%I-d?&gLKI4!~+l-4j!yCAPw~5)31W}`UA5mNlWCm3;#)XOnU7N2{I;1!*&o8z< zXx-{NjT}1e!iPX{Le!E&FWCpA2+SABeOECaU^{n>rrZIw+!Xu6uMY-|3qfu1c%IW| zgurwaPBqPTY&mpcy!<7gDBgBRw^z=V)7w_Gd==`lF^#)VF-525+{IgBH9~!^I4GTZ z#{x?if_>Y|cil-oT3ya}$oe_c0G7C$I{*5puab_gKJ|cwbLZsM{73WWL!HAMj=3lf zq}_bKf59x@6XS<_QXkgqHhgE!Rpe#=eU6UR)m}Tjt1lp4F*u+*U>0fspDM9d=dsd| zm$+D0FgP+^a0P!V&00^~FFq=&gxZFdS5oMk!aoX;v&fASSm3+)So0g?HjsFIa|zqk z*OI*@^B~?XnA7R$>25jiJCN)fl12Yj{5ztxmD>q4&FXd&l@{wb8ARCt-1rQs=M}L# z5~T10dGv0$JQ&!AfJYriGYCh}J3vw|R5eqL2ex>QMrS}w>UIaVp9^K%xt2l`LAdq$ z(Z=@~d?N(2QoL;zt3}eJGgcy7h&XS}y-49O?~3Yrb8W-be*vQmhUI zNf)*dwB88qHkWBNREM;*JbD?9y?55XD`>ldm3#NYhn^EBLJRkr25<7n*Yl}X_i&Gx zt2_G1EzH4Vq*6WfBS12BWV%j=+PTQgcUv`A`<6NQ6}#TIPtUBei}aQ8T0xggduL)k zU47xOurc?v!uO8SIX{l)hfm;eygAmiSme zG?2)j5ar_SczK+hlrP3i4R!#fVTrV(Bdl_M-76)%-U!=l=;^_mCI^uh_}74Tr{h!a z-|;5>dUhEf9MTSv{a(3vX_5H}MSfOQ>|}l|*L5vk99&L#7NErI&u$c(;~e@3T7!5W zb~tecwd{XyseCt}ayz(wK9+G)0^0!13k20H#NISl@fpkg#p-3;Z$W81bG@gO-?>u! zt0IzElh9A%xs%kvEi)z0Zm5|@h`t7D?W!y_W%;*{2oyuwO9avo6lzWn5q zIrz2uii&#$#m?2oLf62+11+r{vFgqdg_@_Xc(jdYz~7h8)Ony=pj(8{Iw{yt(CxN>JwIQ{whjl*|HpM|0UGJATD*Xt~N` zz>t;oW#OkIxZtxzy|R^W4|$E@k8z~!74ZHuC|O~ksER&wM*`C#>;`+-h~){vl`tX< zFU6rGF@w|Sm%c`oZCqsati$_Aif2PCM4hb()*C@qPm#4j+YK?JexMrX*Tw%l$((#t zsNF~Ed?+3v@ladue%c4N3XWAFc(-~uD|*pz*?J?ooNzN>RY{gT-=AXNRg zl2e6#3wG~&yH>Pw-_v6m_j~$G7EWYcSnN~sx4xKHT6@q`M!&L@Vp*FZ@BZA$wQVf? z)4hQG^`SlApZ>1KMZ>%F&PZ{Re#GzO%mQminvbf@`3J(fsTQpTql-B&QOtb|;%@qR z^u$URdQ*oY(9bEa23%%*mX|m=>22g2;%)Q9%~bjhs(0}{dc?g&Eq$Pft9s?l5^}o< z<_nu$hlGLKOa{_?EP@KgDuWMVA_6gCD*4#(zk=j}HRS%SC{%1lF@Zl07+)+F~sY6pSTYzGCE-gG`MQo$xn# z7p@i26oQ|+Lkm47K1A#~*eE>DMx~lt>xpt3@z-Zu_u!E;BsJ9e8K(|+w&AwD#hV5@ z%o}Ot%<_!t`XjFt-eShh#ww0!O8cF?kv>?LN>0zh1f2Qp=h-cqUeKr&wOW1Yb^tyf z<5tvkf6=x?(fi)5n|l2gc~A$>>Af|z3ilSDAE^0RuLmv@&==wEo*7?T5baT%76Z|q8PN8Aj=qT|(h=pIC3u?*hI4B|((aejk`dcF`<#`V?C(f55>*U2G8 zZA!)2C6#BVXt&PmFqlwJ5DThZ@>~slTr(U-XnVvj*OT+G>~NQ-0H!FhYuo}KuzhTo zbsgKJsd$h(-qV(`*OZ_BFhlm9-I2*Bve|JiU=74=kO!>}u~E|V>ubv?)u83m;%a{D z3`J&P9repWp(Wce@O1JAmiwrcd8OaN!v&_H=! z;tJlLP&PjC$s@Pw{ZSk1XwM?JOtxp8M`h77)Ad8e}rNQ&* zDuzZ}2X4w@c&ZMdXUr^85wsY=1+wWkOxC6+nEj>hzvOg_zcnD(9TJpDm4CHqU< z6w+e|AB>&JNxA}7{y1WiCqHa_$#njU&ItGN7YS^y@xyaAQMr>_ho47nef)7B2<5)g z4H>-w)<9IiKGCgNz=v_%Y|a6^&2jv~+R)ezNI7-*7sUo=u6hmArZ|4GPz!d`Sa3&z z*Nf*GJ2tP*$-Hgbm9$0O{cdKpM^5{TX??vDsKW2(3iO`nN9KO0-r7Yz)_VB+o$FNx zhU*LWE7kp^s~fY$qg*AX&XraEqBFmC(m6MOHpI5Tc!%h56kG+WPInpL?a1JGpCTR$ z*td~3Q&zLkoDqTslrbs319PB+fpYPU@MO6>FDpBX44#aCLX9~iG_84ObJuu1hR=BM zQm6ktxsR1HM%Y5kfJVNyJS9yy%nT$#lV_1L+lwg)^dN*PNg2N%kZUXvxPd!*{6=U- z^J_#oYt{LOOKr)dMh1D_<0V^kavGyDt=-x&@<+l^?p@IrfXH=+H1-8doawY>4`hpt z#jnMOd7JBkc9hiO3>fM;$sz340BD(bUQVJA&0FifMd+Q@;Hma*dW6!%9XSEn!EuA`=|lczXzMOK>dxDF9;TQx z;UbuHukkyVy1OVNEb?5fO5cFe?6%>^0y`VQ!y2{oGPL~pouj4yK@7rq|y%2;wmvip|-9%`1jtWUf^l*S(f;{bSn+jQqWyw{*u zW}dK{w@ONd!ai+2!~U;93012mt0;s{5ZO*G)#T+vw!5N zY~nQ@I)^xui{4>nI185&9`6ukS;^1PQ?G@S!L@FJ!W-+|AhD<1WSF_F!MC5GZdOq3 zi&^JZ`kg~v9$PQ#{M}U=nkJ}xU`|uOpMr7?cl^dqXw2aTb6`217jtU-yM%GyPB5qn zZ_P6uqhK$s=ycM^5$sNL*TVH(+PMzNmMORP1z$TQyZg&faf;5YQOMaB4O~5@SJnOd z&AQu>I*!9-AKnesj$qnK9}d-*IV&w3=stY2N#o-%ZvBkDd*dJVBAO;U$hSOOKQg6L zNA*`SDeq zB(Pt-Z%bf535-Si)2md{jQWz4+<({vJrUZ3QRF8rQ7(O%i2R?s^nDRI*+M5C$2fz|JB4C??kd*-P>CGg5?C>8JB$w!xPsih zgVlgrA}3sBEUYZBYyGtO5&w|Z`Z2@k?R(XNTvF-&VV@A}mGza0$Vst`zaW@6k7)T$ zOx>{RBRq`FYRUn_jhU_1Bn&83&RVtUWEg)t+%KFi;zD)#8weQjYxnpZox4r&GuYlu zZ|zUPns5=bP*dwxXmG2NJ=cv;3nlCVqi02^nbFblKKaAA6)#{ehx*EzxB;UY0E_oz zyEO#wZ;w&_SRzf>(~{swZ`RHd?c-p%Jpjpmn2w1HS3ygknZ_R#J)yw}ue4-3I^L{Y ze#+Bjlpt<2h505|UMl96Ecyx!0Sz?CxudoC#k5Jn^9#+t`91r@St34gLF{^B<4!?O zkgwk#Y_S-ynfK&G`i>WQXyJ__4qMxBpwi z$3-fHgQwI#_g3=bM4eWVoa14A;*HSA7T&TJGc7H;E0iP8GvoVjQ1a=gfl+LrlN?;Dv0;t$E{R*~xY9tR-|IBmyQw7BzGAq^e&Q>3o=2LSq4emgZ@Nh7suK6$YP?m5jC?$ctR>=V` zur#2t%mup_3*%r`f~jVA=Ns@#;>a78w`2f0P)584FP2sK4z$Y7Et{zm}% zW)bPIBQJr%HI7l%y+!jYcK7|j4{<9g4>0R{iq$$?w5`n_k9IgvQ9)svBRNWBLJ+Un zBmX!zU*BBSYce$IeMq}tsFPEiSzP;T)WWH{%5}z%{zn-hBM%GI5s4GmrQ==RnouL^!#((z~*V7#vlVW1opznPl z`;)h8|8s&R6n|P8@6L2uvLRrggSSGbS|8=1mP>%8r_+YKEjOV_O2Z>v#(+5|KR|s% zs7P_xlDQi1(r;mUr&(}md?Q+}?4laN`N@OUq^!}4P%}aI>(bqo>AgSkc-JCw%#LVo z-+uk#xS`uwr;)rAlhb=Ye*B_;BI^Fn5sL4#tW0-DU#DH*Zr`k)ytlp7r&F5NbfWl! z(+e-f^nA}SzJplp2-LFrB^KR`6-(nG>Sj*X;m=0#ADe|HPZK?aW^#Anh@6xjxT5eZ z7!(d9vUB?5NXc(-tZsVKS{6QzLrk+!#QF9xS_+BV=@)Zkz+v|>A&L`jJ?xon7`_h} z@#g!+SG7HbKW6sN1!#&oh?j_&ELkdI7yi(=SP{2(1#$zA$eChm-0Qi1gbdy%I-Wz3 z2W1&F%0$My$%+84Z+@FD}HWT2vWXzewBJc2%L@&vHmt{{hO$e4Bliqt~gt zd-G#mFE!M2@ijebCUeHYrs_=?`%)b}J2m(GI)R&G-EfUZ=AHM-#@OxQyXg~*fKMNK z$8Vk+;$QK}pHm`0XyU*4c&O1P8OrEIT7M3KDTL=lDwZq9p=XElT4x(6yp`Tl;5h#( z7%e!7^AqnxY=goeN-Ad;nA2O17!k_G-47+~84CjbFXr9^sHv=L7p9$1P@^EAAVft) z#sH+=j})iK$=TD|Ydz~(&%UP`IrYr@ER_3$R`$tz!$dOo zMBPD6o8>64w+aaef zMw$QCvya06V5vH{OXVP6P?IhX2xN%lydzL0dwwI~wNyQtg5u9}a^slbu`DbchsQjN-=JdE= z?gond3L2W~aXenH-FYFpbtVnr~P;qC3P zWBzjeJ-UA3c;oczXI4cu4@~-}Rg$1A;aSN|{CUBC7H1$yuSvEP;%u^hmEF@syiM2@ zd$Msrs&UTW9l24kzmTnlQGdl&_i7!IpNcr`M%~La((SrtXTDy~Aaui1_qoY#m{@VP z&AK>=c7aV=?iipO5WnsMkil#Z5IS2Hpy2{%LI(??(BjXn$V?n99rqmE_ii{dr`}oU zgzYDPVc~ob_N5L1;;OWX=Qq;b$Fq!_s|GSXE8a_et8kiQT65<7Nl$Vs5=LT=W|W;f zi?{8yqKnTo_2pOh5)iWu?n%@=)1E;^Wt`brORduajmF3R4fh%d_tzHYroP)8^5Tjc zXMA5vAL`Ql$JNeEg3_#tr!SpUgWt|)#f#x%x{;XVc%`&cUujM#B7(a6Ijjes{(~PI1{Wd{^H+EYc(DC2raB%##^4;6v z;YVC<*kz48x4AZbVXiyjx%53K39l~qI>DDsqN?Mtb^{iU9E55>?G36)@8q5NR;gZJ zAya(3>_~Heu=oDHnjMApnFq4Xaf5GPZFIaD3zA)N#rzIwGU_JSlPUbA6&bz2uftOH zuN>O)4v;sR7rPJ85mKBHkxs?CugDxn%|)%qBrik2q;;=L52_t*oEdJU{kb>I=EuKU z8^5%LQ<4pT^H^Z0|C?WB{jXn@GCxk6FkIAl2mt=odvY{RAv75RRcSbN5rQgY$yN~*4EUu z!q=QT_epFZ5GxD$ILfi?@X4H(?CPt>H#)Ma(>isZ7hHW@)%eWS>6Fp}`k$i>rZFWR zBjyKY1Axzw!%7(4B*2n10kC>e-4?wD^7R&6i3lM+v!(|b&$H;+-lNYl151u`-;7rV zW>#$}|NMLBzS65^C+DJ1t)tCVB5v;OqZ&ad&gFb|r@CU>ii`~L!id_Lo+jrfAE&nl zMp&|5Y8!R=FfUN+t21$`M+&Oz{Wec|zg4+&^x$-M!W>9@uaPqKscIf9FPuRj_STU7 zo#?SC*>={pwjF$FR)n#-?oq*Sbit~F0{LHh=au#|s)=x7i(T>A?~eP*s}HuRB5r#^ z$Jx$po`?MC+9O3 z8(C)v3yu+m-50;v4IL<*uA@F$i@Em>pE4F6s(+cd-~G7GIT?4(@6k%!Nq!L=ogE<#<@u0#E=9)!IIqu3a1;gP7PYRpM&;^6maQ| zJvBkWu(*Nc_-HnEH4bsAKAp2lcM#{xuB{!_FS>c zwvY{)eg;q?vdQHTk-c9+B0U4|6ooeg9$Or_KkGkRNxQ%jyMw%o6z9TN@&~GL{CcPJuDkBm37mW3Vq2ivZ@83o9}!`^d;ho`hyyEwUi77&ZBf#63_ARN+jM(?yVLDG zxqXf^w5>qW>Z3EK3>S~xUKfLL5?H71g5sVAQq`bN3J%_;fNNo$A{~&MxrZ*s4!MX7*EaXUzEO-EMpza#*)H|MrxBFO^vRBXz?dllkVTSH#O zvj=gU78_pE+J^xuLq&Jl%;cyWfo`=`v_49(0(&fQ(m%N~MDS!!#0#jH-%9MV=EI$c zpRxR<>*VG21Cw?CbFHGGu)oaQ5s5$g#};)KC_dx}k+K6)^(B7icX*<47$Zt8$Vb#w zZ%Xm7sYmiyzdvuDqxb*WB~hPcp9`uAHj zOWNOk-uizw^*_y$zjxSC0&vv+d#}0){x8P!Z{PIvW|T0azZ$!x3wS^}eT$pCP7fHd zp!3AIOyZELm87EZg7C$DNzr)r0PluVx9rgFJDd&sV&8qeV8aeO^ZXIE-}_%(MR6FY zPbtT1Lz>`m>!7VqUhy+0;2Wi#5$M)H%oe;ZWc&!ft&T=aRK>JO6u76EV)KUqL(D>P z!&&|s#i4*SpQPN-BfNK83mYuRm8xiQOm(+Nc91x2+9aYYtZ5Z+C_@U9J87@ z(+HJeyaxX6=7%5Uc>m8{m_JxB;sBwOC`?{g~}U|5-28kS_;hq1cq0d8U<+7k`+JSs#qoEJo%(g(pb238?v8 zY_e8sp1QzjbPoedX>tzq&!190gbUYSj53*U>(k0|+jMENfy5ID2H0!KyfrH_3Swh^ zYzF}*Apu|vOj-O1s7!!pLGBT(Z3fgS<~m|X3*hR!>>ku24H5$ zWkLj@|4`i1QsW1#U-)I|gfi24wPnRk^&V4*)8OM2x zJPC@GV*Zr&VpQ;dy1+8r`o8L^#T~{XyCb}(HAIEl(gI`Noh+q}nNPr!JCNfCSesGowf%%cgdXXh z0UmM}E|3FMRuQ;Zj%$3Gz_~?ZFF!73+D8K`9{_|mkt)4&i@bvsPt(@%Gg{O2c}!;O z0_)G{}p?OgTYJ%pBd9joNe! ziyE$>0pVPoO0&?493gyboBmLe{{BFnsg&e$?RL|meuaa#htnK3h{8p+*gS!A%Fttc z%3TwUuXN7z+xLY5Xa97(n(UC1am8Zly3Lz~7cEK`Hnc2^XU*%4kc@Mraw>x3HZ-i# zSs-hcStGK9NAMUC$T$XsA`0sh#*aPA)T6N4mqUd~7uX_8Lky#O6UcDcMQOUwnrr{?7{JoxHR zOk#xZoeE=0QcAuDEhf~`i*4kiSBJheiV!5wbImrHzCvxmd$7~m@!ac5A; z@T`f+_3np$-I2xK`hQx_12L@~cy)*v%13@Zic#D^^c|)oW)WmmvOm_lW^wS@pQu{W z!By{p#n7j)zyiJ*B<=W<*#8}4g7ZD#Ais5ryG+9ePwdu|zO|@y zVV-IT_J3`tqWx8mitIWvkapewMO)u1iJRQAj}bRT$SZm7v+VaVLPuflJNz=3-m)k+Sqo)Pq^7u%$h|%nr|ao2`X;ML-tbDriHAnV!NmK zvpqFDdG;uUqp#MXVN}^8)-8b)==7!RJ6Q*_Y#E^v>(=EfLBUHy7MF5m!JMzAi4{Tp zB+sTiaG8>7;Vy`5Q6?CFFv5aDJ^nl$vD2E3h5^R9*fK}!f>|Hv zg?MzVU7_mtVm()FmwX#f-TQCz9VYF~{)pd`VRLe^lu5VCJm@Yw)7DaW>aAjvPFIgp zoJUvtA^VfLM)SlNBAW-_hFp(FNHUsC1&2D>rpW{y97%Wqx+I`*n4>iu#LmG04Wn9t z(^RdAnS0BN5c1kj!!xukp@F(NsbMZPf^8A92|UVg4b|1nXcF&mQYkDjU<%3T4>%Ly|?5*;t!mX$CE?(1VK)T-Cl?-Xq#&UqYyU>GF@r!V8 zgAc*zSH(Z`T>)0jgKrd{s_h9S!o?;GW3?!2E52!~JrGj8+x65h0zRm(XLLV=GN*r; z;ZUcX?{?L)vuk++c9qw$O-)A=$C7s6R`%U{NRC zu>8w6+kgck3A1pVU`QO)h7wj}O72j6D*rt(Fk_Ftjrz?HN8JOp3`GoELJ(Y}lC!VL z->Ijf$ic48jcn~Fz(6)Ubt0d$=6TX2sg-b`+Rlo(KeG6lU8El; zYh*>{d@#^^KC40I7p}!p>k$yc;)u6t}dx+&ZBRU;+P?DRcJaO-G+57ya z(#{P|w|^}Zo6)>pGE>jC3+zRzXX6mz<~CM`OKo!h45-$0XwtRzXp1~@?WDbpP4G~V zflr{9!{r=@*N{6~wr&27yBRqStbZ-8dn+r6ypgbvz!*AtpO59jTcK)GD3y!8rLmdc z_P$DxwqPzn(HoTR)Uefvit6Te{O&|e{s?JkF)&F(d*8|Av1N2D${zvbB-Ww)S;HEW z!g?ro3`7n5fw8)1f+o)Jm2_=Y!*hN|?3^)0an|f)j^EH^V)>jg&>JW087%S`wGUIz zy%iDn?ndU-?OQ(8fyjLw=)Z0`yr#fC4u6R#h~!e*)vv$I4z9|_siJRd=z-cR-!YyR zhA|h?AhhF}Skte(;xqv~UB$hqQZ%i5Y(w zhq?v0)ccjR7nJB2=J+gxjLYdfx$(T*8Fn()=0)2v_lf6K_lhTe$(?NpJtE09o5{a@ zDHvxr?E1je-L`1Y@AiebU$V!~{oyFS0%?i@@OUVv7=MFLEf)mR!!U|XmqZr;n*z(H z+3-)6vUvgUcoa-*_i#GzFhKS^o@VoBP?v%3Q-yy+9wj8bY-WJ5=HQ>TT1;UCi|1M) z9i?rNZU9NeSLXqDiC`efEf&bqF)*=uG(guCaORzYJNXYgxcYW&RIQ$hq4^H?e!hIt z?vztG^kp0QAgspYCk$HC5B=1bYtpyy^+>%@pi&#eAGYOVk`We{!nt*o{Prw1pfg_6 zzcEj*rS3p$TK80&Z%gh#Gs-`w+gEwqz7bZpoZ9Cb^*IG&bxy3v4;pCC!IGSa{v-`g zJPI5C?9gajuWbCVXTv}}LmcDq5b@>np84*M^jd`0_-Ocpoj$H}!=Jfb+ zX?vrSgW`jn2NT7`7U?9N$_1I^p+`FG@PZOY#Jf`Mt(%;gCKk}r!y|lDjAG*5ns?bLtdLwc_CuO3Z6+Ij^;$z~7zUWjSYs(TN zKNyYt^0i&6+mC+5b8pL9(lBDyc%7=2XxjA}|M^ID|9$pAW12uyr~F#9DT!~{7HLV$|mj!>O80Wrc_2iN+E2-bvxJf1=XN$4%L=D#iQyN2&$bX$>qKd{mbQ&c zvD41#37*xf1uJhuz0;RGdWB&H&AEsmgUf~cwvUzD_TcYkm{lLVo^c8@d|YvRSA`us zlT{bihK7UrSBq{z#hH+|(1O6ujbakkK%#c{QMlwbUi%VuGksl0~QcRh~=`zD%KJx~4)I2^3%4qh}rpSv`dR3wfRblfo_Cb(nd35NnP z3StCTWUtC-T_)YaO81}=sOzCR&MmhH`?6_ZT7nkKSnK?y`>SfDudC~r-*Vl|bJkyW zjYfqa2Ha^FsZ#tk+bOw3W36(Qa#4_$@5s={a`bfl@sao#MUe!V23%_groA zClS!Tx^~QMjH-p2=tLlSyV$D?M|NV+{JNFBAh&M!+NzqISEC#IElk;nZBsrQ>f(F* zKc-+dTT?nOD0=6lJXc&AFx>irP%w}rR)yA%!J_pAGLW3Q!x|u(;YEVulJ$T2hTUU- zWAbEAwU)QB^7^&8F(3Whbk!;IH*EK%ze8ST3g6BI@B9ggL|K6KD0RI zdtS@78#=zs4IOx@RGG$A=Me^|Ms_c7k_-<|Jlo(8{y@J zBLVU)Qcde7UX4rH0*-=drUk+_;#{CvZvioW5wS5~*|(KEq}%R}+?DRZ-zgl`tIn-P z%g+q$)wIb~z07>)?)B7XTuu1P4{V6|PiSa{T)RuMp__elE6B9Vi(OV^9y^N+fVSpW zNhMJ0i?RWEY<&S*nq>W>pf#aW2;k50Z{huAq9}0)f4+?bX(OV)dC}yJ>t5AYqc=<6 z0z~{*y%y|eaiWJ)s2aAPA z1RH5`22x8x52_mW(h|W{NfPfKlVItfB4Ad1^>IEq!5EQ>h%h{r{K`D-)|Q>7^`QDpWI2Y(aFfyZa16iyRgBm%DyEp*~u%*EyLFE zo|gIr&t_%L7uw*oW0_gjb6eOj@IDsWmJX`p^;pr<0`2@98C)0?NXrX}~A z!DD#Ey;228i%sGA3Jf{e)WQ!0)lpoED=}KbpvOWhkj;b9J@4eJ42F!D%zHdv2;FKJ z5W(&%-`AMDUe8#^rSC~cp0*BQG!^czeV~Oo74=(csJ=^yOR?B2sm2iy9c0m!kkD>2I12=eGcDZq(F0X(tWB193N^ z6ysip(-e*f-0w8``?Fa;NxO+#kb6N9BdhiRnwG0hBbY< z&_2+U5z=MaO3-*Q5IUkUHl6Lk%-wp~YsV~yHDoH4YXjJl6b2QtyAy~q5BVMK6nW^n zgP=Rk$Px&7(P2tHv=}yI{h#c(@Dcl69pHbPiVTv445w+(#hHDCxIo4g^JXzJE&WGrQ2}D z97_9knxB0bTO}T;=oLs<&%euf#HYOi(S_JafM@aV+gS{`PQx^o9 z(VN1dmz9<&wfn)eTYHhAx(V$9g^Lc0k3-Sy?zLk5UV^^ZZNR-vLz&--f#EF?NdN_0 z2j$oYTK<|tR={PojPB!)mR{PQTDT_4ZFJOYct@+_^Zw_z?~Tj1y+Ef0oBWO*Zqm)N zm< z{a?+5wfJXRn*+dnX_tU=gsDBUBJ&BN)B)O6?^XOhzB|3^dJA$b6R8Vzg0=S$0@-xB zMAxHNYzUk%Yo99B>`r8J1@MHctQa99d+mS_*+AePFRv4yPx7KMxrEiAPjf&~C~_07 zgM(?q3`)bl!C+!PsEtkBa(0%u6{#lH>_MvI(Coq9wY70qIb`JkkO(ss6mTYlL2$bd zfB4|C+Z2{RoX9!Tf@eI~L(vKK?@5b5M$}!c0xV09QakqTj2x>r+fZ%BEiG-=+l5L> zH|||p?`em7>r}eE-JmzUS9@?6ka^;jq0#}V60iqkkpZUG^sAIr5!uG79B75h;+$JY z5&hCl(s!PDByh&dEOIY1T|@S$tR0wqr1GZVQT|MUM=`#Ex!Z5`LOyRlR;8$ck+;FTu5lxC!y)p;(xo z&mLx|d?E;}Hoht8x?!-tA8%z5K_G2<`t1EXpF@Oqd<4&mO&X#Je}SgC&9e5jAB-`Z zab8^5O>inn4q6FE>`qPmE!Ee8HnQ<|?(#RSiI2sMNmjt%IWsu3%MZ zsY|}xw9kX-^JU)H^_;1e<^x?mGx+qBw1LbSVFNYbY7>|L;TNM%pq4}y`-TUyN0(mYrcK%C{1#xVR zz%;(vPM3IVIW9Gomk?)kS8{h#dj;i2@|qj(cD*j6{k06u#X6EKBL6$#W`jlAkcQwu3)8@Y!o|=s=Wx8BVPJM9K0r}OqiC?-0;Um#|a~t z4b%gK?reBl%KTvM@+245&WdNPt0oQ3zND@b)aDD^Ik4#Tmr}I`NF=hIB5#px3*;HT zBH4W2=}rjy3xKQzYn#Hkl2t+$9kiGQ>0n9Hed03%vh6{E z>`TQh5MdR8$M0XxD*m~y4ZXY%{(MpjM+^h?zbqi1{BPUk zS)~3Cl~gPx!BgdBcOrc26_hVl9xXt$lb>VhSb1re8&IPlCrN)6^SSOtIPR1l;6Dm0fH5|nih`kg{4P|7K9{N##?l-n{n|zh40;=pxg*Tt^ zZ!|s??`15~Vwc_NERBGE%vR|xu^V)ZOC~MXR`O@9`6nVqij%0T@qjVP-H!JcAi1r1 zhnS1l^;YZZIPi6W%a&9USKk)478m{SgQBy{_v6y-UYz^0{9>UuXxq5x0(_E^{9_*L zY$k(w{FgO946OM#{8a%)Dz_wVqr(!HPqUuGZyJ{sHQHb2s&oWOB3sC?(2?KfTc~^c+N=2F4*wEq z0`b*Ja43d4;T?HB>N0Bw&XA+k0*}??TJv`ES#0=i!U0g#bfaA0`x0z-yScCgl%mD? zlE9RQkUPh;WzcxIm5Eo+pNp=L@RaNU-tn9UO!$1DrK?V|KY-+MC5IM zyN_Wuiop=aPzWDs8wtv@RAaslezT-hj9?>%I>#oXL&$fis~~m31vV-MX)acyLR$sb zx!t_gW9WDdIe}IiM*ZA5{I5`C0bU0b+J$jo2~D9~L_$M*4Od^q<^}&#gfxL;X#ndf zp#_t+<7iSjcfc9<2ImKjc@u?K2SDNlrHT`^qqcBjo%<~8h=K@pQ}#T`LZ6M=IMYAW zq`L3U-2%KO6gNgz!zJr>sW>+>%aPhRlOQt@I{7^>_~l_51S}JI(Uy3r`^R34T9HA3 zB?-Eq3<&)E$2d=lF09BbEYmOt9>Ur%iqLtg3La!|V`9a7`UyMnX8eSdMeHESK|n3! zz!Q9QL4BMEzGW)!=U$}VwCDc&Q6=ItjZK1WrmAaMvENF$H8GBb1rdX3BWqjhF3~Rk z|A^d;)zwHSg8V_VxU^))d}x0@&*Jsf7G_!d~CdhdnqoTokw{*<>{UT z=s8fB2W~MZJTwI)rX#%)9IJ`d{+^jx@}BGN*tj-jb-n<)`6#i{FnO6Y2uRS!#ZwCv zsHv^MsRbMHD+zf;#>nWp{mBett+^>?2F_~k$=h?iL0+ueIayyOyBvD&g}X!z6x!D4 zIF#FT)%S;$=|5mSi`S6yo=Q@6e(&x7?Buf|(-bHM!G4U}6!HLnT_K;shIdflcsYp3 zMa6(XN=@K=>Vp`Z<)$=)MDgpQclYtYiK#Xff#cZfO*amQmir8Q(HOUB!$S9@z|ftJ z!zbIn0Jh+oCs0%&dB=9CHq?%RPE*yTz3|On>ZVl>h?KH;ySXTNsQX-Bknrw=H}2YG zi%{t4a)>Z7BVSt3*qWq%o!|d^&l= z<(99`^JW|4aGSh=d})j7(k%(YUbV}oq;4?3Kbr+>9W4HFuuPc z*cY;Uf>-}wTyDsQ(VJJd{s9ptTdCUxZW&K;^Q|}beIq63>GXzT*84GRC^+hjSJxRy z^~iHcIWubY+ZXV!>%pji0ET_ck4R4RKjFSw`YV`Y?iaoZ);i%hd4P2qivpndP8&6z3de|0vcQy9oXyG-vvpEKP5~~RDt=8Px0V? zYVau(IpD~xLleycss<`BkrG7rYbam zRA?Pun~##pS@SDqFq?4JE!lCjmQG#R`j3IC?$Jik4<;|7&ea3Q$j zmtZop21TveR&_13jZt?)SU2+&1b1lsGAE#GhrQMDn%ZZuh+dDQoor|poNT1v`oAm? zm1nY4>A#zy#R>`RW?DQre{;5#ypw8zn`Fzrq9tT^0M#?#m7k=^k9{BjxIh4st%ohV z$818$o5e>yG~m|#4zAl;=L|E%~AJCVw=uCkq+eTkx;5(L-W7_$BFGK z{HTij%Q<8Bks8wOJY%3=MODKQMP`#2D3m|xCIF8hDnK1CsT$w_@sFJxbYh)`s$g?& zIga9)2RgdaK1ZHd_W)d{qaLs4>1L^wi=PLOnDXL<;4t*I)sfuZC@m% znyvA4_R#aJZO(@bJCJg*cKvHd3K_L`^%F?Oy7g{vcnHFv?{7}LUlrR-e-ao9qd?ko zHYEsiLh^)qSZo50y%cD=3F=~NVC#?v%rMcM5O;4BKcIpUXJtGg#GP8?XMkQKiI z;a&xcl&BB;Dp1<{g%TM4Q$L7b);#6I;zPW0b=lUu)q<~V`xTi~|3kY&W&Hc=qX7SyyQB$Jax57$H^SK(ZXNU6JzEzQ*t`R0y%REl(WpgVC@xzy~eU!9L0 zt>>FLNWl4Hc05HYL+-W=ymQI1{J~G7bbc0_^a>k~k|Stz1`F%9g0eDI7gh1l`$9Dq zEj2t$mNW-kA(|A%lbr?p7TCAKA5DS%(=+@g*zgcG@I-e(@t>Z=*A*G(zZ{2zWXyj~ zNvs(DmtT2Q4gUY35KU|kF*xcfFELeNyz^Im`PRaTe!QaqF$RRxb0ay3cAg4aAn!k9 z5d9eUaLQyTzE-MzXqu|_;S1irVn!kHO0l2AqsYTHHtSJW{t@MXEuhzLYV|M42Oh2h z7c31GK>iLRyuk=>Pyv60876R})=~cU?q47H@uC0iMFrkG&y^2r@!8#v*QMZ()l?0L zEhC14tIlgAal9s;(M~-EVEqfmVqlr`q2m^Z%sR|@3I~4Muq$cLut5HrrQCYi zy{h=kwhNSRVi4gbR%R1FG_G%w6k0>qq>oo*+@4{+ia?|heY{hei$bo*mOM~PfBK-p4J!;W09n5Cp4iM~^gY40!6wwi4 zj}=A?SYRtM?|68pb+}cvvuzLCN=vCGz}0>1c^4oPt`I0?!5AL;gq-%aOAr zX5XZ>V3DgS3aasS#)bhpRt&8r)ow={brO?-d7ut{%sitdm*2BGVrb>KOfpAqypm zAPTl!M=gVGcFfhHNla#a1_i1yG{Hd=29L3@B2(#PaA1%z8fIZsa&p63-TJBfNwx`gJvB6)7XOf_3m(}Eb~#~; z^Ucw%`3+`=t1lJxvc#!79Jb5lFVsQ9`uSM2cv92wSq_kL@1CoI4eyUB=YYH+KFJ*t{=5tkvSL9OB(trjL_8V zcup4Gv$!gzu~oYWdm*A!PQAbfif#4hP*y?o77fJ@QUi$mQuW3o;@IqL%DkeVsusQ6 zwEYd`Y=O@%h3P7zN3dZ-FQ|p|?BJu9WwfJpJ>}2dB?=w_0#Wh}zC)i=ciA>#sZ%WK3~k*aw$C-+ArI zM#k^uo`D%Q#%94YHisK^!fckl@&Y60#m20;B`{Mz)1Xs0E-(#Xrh-V6(z3uFpn=)D-u6qE(Z~G3|Zh85o?#}LkWiW+*J8$OFiXTS<%e*X^Y#`FT zrQPI3Bv{6`67HbB=ln4E^%631DiWBl4X~ZWuf{$a54m|ZVl-eFAZyMaTb)OH9YNTZ zmE-~b;!g`LmgQVE3oPPzyS*);rI_x~;_($32T91+PeL*jNFEeP6G}lRlLXzsz_}3V z=Lp@bMGKISVgpKr^m1VP(Y>S+-h-%s-jcUB4IhL|8GigGj&SX0z1>w!EsUcAp_ksN zC7M`0GWFu0=EDU{q{YeW> zDRfx3&Qd3zV{my0)bq9!&N}k2=-&CuN#V$?mI3aIP6=5{X0uDn8h8Kkgh+~5(;^I* z%r0Q(C@R>?mDFV%Ua)y zhe!{m0iH~mk!CK!p+MA4-(nD%O5WrFdprR)IdF%#Xa;!6>>XSh=;~XmpNVxYZP$>1 zlHwY;V9Qs;@|qUOH;X0-vHafiD|-neH$>MN!|3G@v4p4uT0!>^ud%f74+pcuorL=S zr&wYR#H>2Tc7Y!6oga{%=vLv_u;e!s%l9cBA0fA7ZMK{TA0aG&^Lzsib?PB<|< z9DH3!6M18nsih00zW@IfA7wqQ`|vqN*CEE~(S;Uip`M&+uE z_V>n{ur7>6Zv$Q0Mu_H+Jnza3e*7E=&&{9tpkJ<;z3|1ad`uCo+Eh`p`Be9wL#Jdm zrr!a?hY8})SEoKBMC@#tze@VYfNX3lJgzR&w156Nr~eD9qd}f-fq4H$^Y@B`+~Vvh z`qhc^n5_x;Eab{db?omx-jWNV?}7< zAPfSbb4mLnjJ-c+jPZQ7GbQ><_|gs@(|rKD-PUEcZQWBt4W8fTcwCDU2*Sj9~2c0Yo`r&ywb})Oeo~ zKTEs~(?(WBDgjE+Hl#6r8`K!d?`WmTS*TS&<$4@GCc5c-5w0lGE+o)ylP7BXRCaZ( z+lLonhx`>hKG`&9+IajDU873qt&s~V5#g;iTI^*67`D2`sRr8wFiH=Xn>Yw>4Co%w zNwX)+qg*zb(iz99(FzHys)wH7ngnrj+>CMo@E!g#lYP%+;8ir>MP;{ z{MhfZ{cG0&d~j#|sq=kX0s$1(&5ITk3vC53x{XCR7ucvf)a~d&J8oD9O?B`b_up#5|_A7GU;gy~Esy7?2)QW+#xYk1e#2?ug zKjG*?hra4&6k9fysw#FANSJl)n>5d)+e01#Td&s^sEX-l#W4@7=a^{|>>~ z*j3_Hm5@qc31oN!;tMY;lBLajIwXJ^Dhm$GCcWQE8x4f6@;a7BSl>=K|uL%X3tvYM= zfV#K3xwqyKWy%N9YmC`6Q}t;oETwRWb?&3(!~?xKir)C0FC%*?nLw2WkT!i=y9cp! zkYJr}#ipV*hN5nc?&v32upl*1+j+kE15yX3jyo+j>&M$XCauE7w9wIw@!rSJ)>T!f zMdoTAd-1$6p0i=pPH~dvD7Y?_V_e|p+d=wQSTYuN9C9LgJ95XU_#)|L-i`qv3eolf zsbLk0U6*@Z(#)S{7n2lS1sY}Q-(p`CtjJuN9x5zRF>3DQ?MCp{pXJ*`2SAyn;-|p!4q^De-vB z)YKXgx&)VOZh2$j@WiRr9!0quuTC#?$wzezte!#^FTy85-hq}#<2%7pmx*_f2hqNK z>SPOiop_%}7n0{@CY5wQnT{Z~rRq8fGHZ%G^r;{Psf;sqtgNBMdzFTyl*7+_%|aho zdUyG5ew-z(6~%YbCj!2^)%;5daZInFJI(^ZcLT5;O< z`={TsszYY1+U(z63)ao8KdHOXl=(pYv>sO0DMRUS(f3bfdPbV-zLkoGt%k?&1L(}t z5>LnI`OzY)j(0>DHbU$GMFHEE-qVETcXDBE+O*5$q@n-8-Frnfx%T^_apDve1f};_ zkX}SVIk3d2V5Hk1U_nmdd zy4d@yGsa%~W?ygv8Ngeg|MRPsp#Z4#z_(AIva57ZsP1p@CQkI;Cm2-q&r`*t7<|Z^ zy8U!(4UiiB%EloHRS2x$&K?(aDnXvclQaBb5e!Al;8wkVWr-snPB;T zWC*++Rn|1PQ}VILeN!icB-uXJv8+Dnc-Jb=)Z)U4!tP96V*_P>_f-!Uqr>-DglD}- zb@VR=l*B;i>Ayq@2box2xQ!E14qp7Y1T#jnx4x9VmFg9c$KV2^0HSZ`?kFGWLI$h_`RSIkXSHL2U&2;~~ORx!ORsk&wl%-rD{LJ+1sj7?yfo+chc zI~X{U6u%*L@6si8%YI9TK*#-!rCU~4>PNTHa}vT4^JdQ>Bli zXs{OobMgl^Fg27gFpJM6i!Ny%48Kl-sV=EP0xs;ck?3iLWu?-H;YP(R{EU-o?Nwa1 zSL(pB^lpC$<7ST0hjNWKPhfH(PfrEs9k^g<O;fiIIcX;UKt%CG!r|*Fa9swuV*|FquH*!bW>1+3MDn(v_wSeEuHlyEE4Fky z8>fruEm0ErXS4qu9ro*g39J5pL9YLcpRp&SJ-zm>pvQw4-_UL7Lmj1)`|e=*|4*5> zP0ok4_&G4%BGwPi0T~CF!1#uwmrMV1zQuoY6xwBGvwsKWqBPtw8x|;ajAM+}xkE)X zTl}))r)}D1X$(+^{f4c4qD8}K5|nL}{VbLqc?rJ*g&ryr@`>o(634@r48P@*#aVFp zA_KiYEWzK*b)=6y&!9LCZnlq7q6u+Nj`yQn#@PP-f>?q(OdGh#J=27}2eq}t@%Ke@@;MPl-eeoiG z!|kEn(?HDpx{>w|!A}}N;C$gVhMFH9y6cstjQ2LJ>!kMVSA9#;R|;xs3VOCY;Yd4s zMC0T;)+ew-->6HP1e^D~wRBh~9d>B(=t1%m)fMy)DGR#|UFb-H70h@cpXmei`t{4n zshI26x|l|=OrP?LUTm+qY1?=91;F}(hPCbYWI?^H*gDO9)R~50+|Qm{{KW7BcNk9- z7g8RW>d<_2FQrO^5l|h=JE^;Y&h1mFfgk10McgS*6s)&$KKOk7yk6sV z?xhdDe_<^;AY?k(8To_LsPM*z!M83aX&6H--DZX@IUeP!?)YgwVOQG^=!(Lq&|xOuvD}EwpvX#tn;;rU(j)x^RE#BIX@~Cl$G|y4UQSZzLg>#DBLNO zqT)=I+-_uCRQX|G_tr(~ZSodkFe6VwKdAq0Mv_s~lhDvtIs8(|!upb;gBfoxTwTi? zA{n1aLlhRY>noN7+vhTxp`Y!O@u4%r8tSuN{P3B05jgdN;V@`WKy%V2tnpN%&TtpX zlUnMryjkHJ`y_|TD$3jjqT0qkd((RS#)V65Wl^nyV2AnAi}76`JbI>)W6*#KmV<1{ z-`FT$FIJ#j0A+=atnSq)4r)sFem;Iiva0lgW%ZC8oypICFTO>YQ!Cb;L;F-?rFTb^ z7L^IYZt%2>v^rIFm;TVsSxEa9Ny#LLkeX>=S|7V-Kd;)YU##C%E9N56@hhdyAouZ( zK~88|6#ArRhJ25Q6HjS(d|X4&0W<0cnxo3F*Q`xTgeqLI0%K7GOrtb zwm$Vf!I4(^*lN}@3@opqofS0B&Z~i*ubKqQ2k3?~k$w>rMq#e4>&$q2zMXYd+Ir&k z(+NG$`sP1-(v?dP@*%>kb9PzB8|!cQg8_W213x(BeyHzLd=%ZdCZP$0i25-7 zB59pvj3#n$n=0Ri7eGdWzK%?oMYMG}CF147VL$f>NGQC(6gPz z(Q{eAJ}V**ktUZZsO^-yyc6jB*6V@+b^NYvr~Bi0>6B;u=OVf4KVRXN=@i2iH1DpP zN*vv6{s*Fg#u!9>N!y+*9?2L?d|S3wn{#|1X7w}Xb5wN7>cyZf@0bU@JzTZvZxd~< zPEMuU9Oyb%=(B5!7cgr;&=`|_u(mGV(b2u(9e1d0{R*md%Vczoxw*^9Wp&q91b^5f9! zTGdXbZOyP_rm0u-J$zGp^U4lRMj5luX`%^U10_XqCw`#eoAkqWC984>TNd44Wa`W6 z;P}0~I7nB1^7J;g@a_CVBCqe{+R%5BtxAdQEmoCBkpq-f8q>&QUMd1ke zH`5p8kfop*wl4(OqGBJ5GAy3R@U}YTH_&DKW8a?sJA=*G|4iq(*c4|bsh_xlZ#X&MQ zli?_yP8Ho~XkAIj(b0EWwo10Cy2v8=!`s`^$hEL~+I;oF4wUFI$3%2|el+0p`=n+q z{(CQPxmX(g^=;#0WOo8VWOyU>CCn`(pcP#{3fc?tuSa}nJyw*IdTQX2+@-#suDfb; z4xkYkNLEfDUG!=sIjn6AFUFr%ER;6f1KiRhhQ`5_!;J*hv zg1xgdT}Y_%8Qg0V+5qe{{R)-0Gd?O1AihVnG>4T=xwmn;HbI8yM&88IV>2stnF}BD z33+#I4fE{3i4TrX-MWFVbn=E;=b26ROX?^|8o>fDy_(IDlohHi{XQ7w)~iXlBK!T^ zJEz0CFy*e8SI~-r`4jy+R)rU@W{<}dX>UlYN?0p>-Y~?`B;?!N4WYDK4-enpMD?Xi zb8Mp{>M$+e81l?7 z)22R}71hUz_LYJrV%y_cXfk8*uX*ZtiUWv^K@Q;^yR9~2c>(ioHJlFPi1FBJY~j*Q zu_$%<4QNt`Z3#(Ml6F47&?H6!X!gT@Z2I7nh;FU!=(pLmX~}W{7V01idDZ>m)E?KT6{$eteX^<+vgAgw z*|?8&P{Yt9uYsDiB(`*7UYwRby95k_;Lo0)z7A*-&CX^J>y^0N(f#uml;bXKv2|RO z7dwcke$TGik7b0~o?Ih$}H>PhnsSHQi{V2(D zQd0-tsd-vFuzEUGbX+*>0_|3J=`USWLQaojiJ z4(|Cc_>BLSOhIPCKaeVTdq~iX%kNcc#O!*w%$76=iUAf{=xYs=oH1IK?K=%kO-8bl zQg3D8_npMAK0F!D+6Q)T2wc%@8x@jYab43f(3wI3;fF|O@)7H`p83dF(ErHm0#ioxr)-Nc1QrDnqFsEw7W828gv^;IxL)m`tv2XEW zWsuhxJQjMgl06Q3k{&=wYFAbs$r1))yGD4+es4$mj$@5BcBPu&?&}e@+BjVJru_m$ zye92RB0nF8bSMCzeSvhR^N&NUrM{ zzxd(RicLq7w-%;g(qmKZ@j}Y@jr;zl+H#0mGQa73lx+uAX>WKq$)panO^^3-iq?`E*%_-9!;@Fz36q99gu|jrANdxe>v)%UsH@QU@zfTY)>(U` zSEqniiv^`RopKcx2lGt-S_3`pOjXRf(D1otNJH}PN|ahuzg&y}HO>|}KLBlMfFAfT zhDEe!vBXx&!aC6sQ%ZLSVKl9dQG|{JT5AW`(gw3%#5`|;(Ma}RC|U6zK=xS8==!W9 z?YeKK!ZuThO-Xrrh|XOXo*q}gA;GHTe;ZN`b}}g-JYRBKPfN%K84Bu}V}czEU14lm z9LVm)_R)Q<_ae#nm7<|hC}wI?oLp(Chwps#$nVMn`@mjBvYxDxghyKfET4Y$Fjb40 zeyJ~*GiM0}p-fH!rIW^$T$+_GRR^1csL>J&f$4#m+he?6hP5khJEt0rZXY0c^HPV$5kD8i;Z&%4WBm4M{l?H?AUdE|~;k1L)^y(Ii;(^j}(R!A73}3HzWGbpcd&YB0}w zaam8_byLfVXSInD2q8pQX0oYNgB7F#pe8&&S-L%vB+Qtul*4!jIV1dJi@t;{R)_1{pce95yexbj>pI0nlzH6vR=A{` z&*rVGL|66br$UvX=)|P>^WINWKAs3tQOP5SuVJuN&=fu*<8M-ecSJ7r`L3VP zFIsU>7*S#|oCvv0Y8`oM)8RxZ>I(6@5Tg5$F#)PAtJcRI4JpAq(BrZwHg~*`5(th# zg*_eL2)IbK4uhjh(CPchh4ek_G!7gNie?kl0AZI+gsd&bL=4B!++$Y zv(i+3f@VDoNW|2aVnZ4TuL2un^$wg35$`i=Xm4pR7h*7N{l2tk<@9uKHiE;x^XY)h zI@Y@KMQnpD%CX|`xt6f_s*m{UvtnAY=}3bK!W9HLQQlFMTVh#mQ^vJ(V?~D0EsjJ2 z69UE&tn20CIm%&kmxk*Q7*1~3{hRdsxNM3@T5_h_JN=Ij$i7Dfs*@;>@3-W z{PeIugh_UqL-%IEM8o~fSQWDt%NzqE^Sh7jh1Am)Q#G}3sHnv~#fwrl_S+4Tc56pJ zzqD$IFAg@M#sgnp)y?cbCi52>(j~9Y*Qr^(*09P(2t2RK`0{dQY7P_F^-rTm45Osz3Q4mr9rfQBXKAvfO7cQ3Sq6HWpW;cnipw513Y^sTG#jup-FbI^9;@O}oglw=WjVJJG zdf}8V|5=_lE#(iL($8AGB~Mo}x4@6MiSR|-`ICyJxEe@DRM_KM;atn3{-dTD*X~!0 zG+e>v=Ln{FDae%NHr!^H8hC09)0!=*7P=W8ezNJjikh#tr(5nhwWOr@wSmOs?6N8k zb+MWu>Zc5s*Ing&?|ax^J_-IAG*TD|Pf;2$Ym`t5kZ`NJkk_wN%G-AJfoUK3y!EHc zU66)>b=mdR+x;G!_Ybs765AJa?lK!qBwaZB&~hLKMwWx1pXT|N7py&b1x-(5)hJKjN_LyR^iGPNe`;XqUsu-;NEyHnI>LhJ1iKp2Jbggx#{M>9_L zON-V^^D76Szgh5WZ2ckK@nAJCdUE+1S099rTMfto4mI(Ft$ag&BLI9VnBTkSa|qvC zHaia>{OPLma?fB9qa(Tb$&)}~bW?0_HK1wVX2fv;;_|}m#fzw05Y_|&-UJ*6SpB8s z(eROKeOX?*`k>V!cKhRne(!DABKRY!njxAnfD3e|g=Qpldf{;YKpgV&|9Y<9T=*Pv z&s1tvXnPIal4EqN!@qtOjgSvGb?w-&aO}8fyrqR(xB`cw<#zy@9*8Wh$ zaLu}}eRZJi;wTkzrMa;A&o7zr83O%G=HF@;H^@T)`+gr@n0R-spp)-vH+QWMf>+F_ zkv2j6!zx4R(ou?p$<&DbRMaF4_YK1O2Bvl81=;e(^Ak+-!k3?kYf>?rQTGe@nASzG z7_$|1Ru>_Z#XVMM18ScOMRlPvYWd+nKuCJv;mFkFwL1{fhvh-+9;t7;&_YLTPDlh) z#uvuH#Rq{B47$0EebLAMp%ilGviAyF>K)|R-{%XU4hvaF#OYu`EWKYjnp3x3F>+|- znU!xE{G(@naY5BeT+|!<6kI5qd?sCofxCXq0Cu3*Fo9ED#F0lGRvn>!)(f z)Njozor^I5eI7M1CUrG492ZB56h?FGQI`HS_NeUzpU57lsoTAe$eI0wsRVO2pjh1^ zY-6A6GhF&mm}J-FRnKv0$3en%dz(XL4W^x>FXa_9zkG+4RLg1s4{RIZ)h_So=x6Xk zMIdckqAVL~90ozXA2gD-tp7{kih0SgLqj7)IpqH3RJ=YhrU%c06D#o!R$nzor1@u zvtPcaald&JX{z+Zjswc7{BThfXNDVlQW+Afs+)4gG(9a=ap(cE-PV3`aHOYi^D>>I zd9c_>%EUtIK>MVEH}>z}g=X_n2*3QP$s17;Wl4P}C9H(tJ9t1pKQ&$Pa8x zEj0o#7NhMpA^n3MbZud}dkC?S#k@+t)M56Q;nB~!QtwTwlf^1ZQJN?MdvTAJO6jzux4J2$M0yMf*-lQ7iA~@tW?&RUnpdeFQ0D^1J&AmypY5 zugs}Z?57Nh8pt}--CUf@)xjWNU3KE2X^-P5LmU}z5N&EC*aqd)&@EcpSV^4B9 zKHDNpZu%W&me^RkUDB3rmHPOlIyxZ5;AJl7sMKQCGNXCgYFC*MwvTP;;v!Qefg4CTJw`C3#_QlMivZXm^GZ2HurLrvGJ z!fVT*IFZ?jUi1VqV=5KKsFIQ(?*;kL$;YvHupnm^p_L{k_Rx8nX21Ae>s%RxXclQ>vQ|1jd#IX+@*wON?%(xBAI1h5 z9|VQA`M~=xLXt=uwD9R#f?;UpRVH@h4&(N#jr)F8c<7s&z=_ndCif88Jj@epQO4XB zwDf#T7E+{%5jvH>Y?J4FUl89g5>K?6_DtDZpJ>X{`-A- z%PpRUFyp`?xKgVk49h!@b7F)L!1!=71FS6&S%c^}xWiDpT~;9bCIkIL_WTUf^atTa z7hgFd8wLF*D)y!w)t~)@^6_8{3Q;$o|ml+^0yOH^J_$4i{X0_JetoInT&O+pT7DIYYrAVo9}1|*t)sA{@d*k$ZMaW%A&;#;Xs9al$->C94Ky7;sOCISqjvZ%f( z6sJOO3^}X=>Vl|utgLK9NAo;*K!(fq;j>Zz?dt*7(=k?wODG8p8Uml9GZyn9^w-O) z9THdSqD(!M-1Gd?5yc*ZL0dvmkS_Ap5)8IHg^j(wm;PMMC$`NW3jdgUOYVsZNpv{; zETp)P71~-u+aJyytmtpl}`>Tqc_>Ym1{H7o1RKXvW}`AlSC!ZgiKvkP|-f<<75 zH*x&==hr||2aL!FG#_hjUh>}eEtN@$&acL!@eE;>YCOf)ZTfXX273>+F!>2&1&%dE zV+Kbz^Rr192aGX{us8b)XazW#ezU{NV4YF8@Ltz_n{n~_jmGt9!}a^3Yc$ouJh9Lutfj?u3@I!^W zqi8#3kVno-R~5(7&;EyoAyV{d8EsP3E}@p+{%MItxOqadvy=|<-t{>F#_PDAew1#H z=5+C=eTkAnze$c0Y2zUe3||uYE_}WE5y92n$!8h4V0Cov9(UKbc3vw3a88H!W!o3~ zJr4vbP5*|=AM@ow(=0+(~@q_zB|FR>oz*t&|#RzCI4V1xhl&faDhT+JHq zEtA`Edh>UvYI+eWzW2JHJbBWsHxna*1WidHT)rjPQC~gm)d6<_yMS|TkEV=n^ow%{ zIk6gDdZYU7i2qs_6MvqcpJ3UevgB>|I@_Nb`?AvyETgX7gj_!=^2+RXLb249qWYI6 z*3TcY#7>yMG5!@ZupD?C%nY)Y88Y~~ez7FOkrIqe z+^BwXWwD2uQT>3Y*R4x#Pa4y|W2NG2g(R;d2m##}>l*gJi;q91VbFAjmH@Ko&r-XW0#a<) z8a0zMt5RQ9;dtED_8ljF(a^VtWmBFx4G*Gc(x*?zt~D^1k{vW?g|j$bqS4)kn(TUbpPF5amF~jI$bT-Cl=qiUI>UeC6(TRtC(|a* zSRqRHI=G?6|AFkpk8ELLp-`mkqu^#0k>U z7H?8JGD*T$TG4E4vWhs)uTHt+Zd&XhQe$-IN|#TNvzLamCaD8f6Y-)FWEZ8Zw`Q?X zv5^}3dC1W={3*o{o~emz)gaJ$3;OB}-7fYV#uRwdj|OT_#rkcT+d}&uxKZM}h2t`P^ zIkoz=U+e|#LB&=7iRLK-s_$^REBk`gKmg&skY&Xho-5#G=T+GI*%i^lq3=;^*u|mPwt?tAgocMemY>Z{#?b z#b|bOyQsrk0I9Ao_k)fARReJ!xjErW$v=M6T+|4mVYIm3V-l zfh!tFYo7t#aRM~*0oXkZD#2f&6lpP$)Pt6N{A)jSq}F-L=<5ch+-1fQir_MyM@ukT z)5<*J?w4`uPg?%(A7?KuCrUg-<3S`MA8=r|z#?GBx8RXlY7L)hUS7mfxt{jGTnD!F zc7er+1_hrZK#lyxqe0UBLpn>Z*p%>J`-`{x6(_AL=QdZj1w$;-@2IJze)VErndVc( z$Ev;jHHF0JQ$%o-oq_Io8JGT4y%E1&LC4zq1C0?=yE3fsT7>HxxbQD;84lx8mMg^{ zqhj11T-1QAQGS$ZewFi(9BQ?QW1}Pmck@LUcwPyxOugbBBB5*?WAU(PWDd(;nP}IY zCvIv}EdAB|b8MRDH#&)eqjT#TSNG?6@zFVH^|R13%W1AucpGRw5#a@K@CZtD%T67< zHGFgt!1-5bk$G-e=R`mX3L1e>=-YZn*9|zH+CzDKzUA4%{{5bvRT{c5*gte8GoqCc zW#zA&rIb!A%cS}Xi?^;)0IENGF+O~iZeNTv5X8{*fNM!AZV@yh%;#de?F*#&zoAe{)izO+n@ooRd&WBg!`^BebOY8!TAWQW8^AESuySDfTP+VEaqidgZ{Ib6R1;2Pf|}?yb`q_f&K&uSt#6 z@N7W|7s+E%zxy-pWXhc|l`oUsJ=+aJG4Y#O&v4&j4KYzrVTv040Tl{xptkhZh^=qE z?x+E^xvT#`gc;|0X5%9r6e#gw4eCU6MEH+sLr~v@XGWYhD)>4pDKDTd%x*WZ>e5i} ze%DYX%J6-R>xUZ1#uO(_l7egNN4H#Ot0x(T&Z^?8<-EjcSSs7VrfFSmoW%J4meH(h zS#DjLgp*|o@o2ArcH)!dy>&k4VPrH5T>hil-DiF6YH@y8=rhro(xXF3!){w{hmW5+ z+c_D^cFY#`93?CB_t|}E(3FI^*zgt*)xQ*~XjynW)3;Lu9~8LNSfviObN312MOV#8jw($p5KK<(1tFK4)j!P zZm#1C2PJ)c`++u_qIHP6ne07Uc=$*280Zfs44~if1S!Q{rGLCaX24?vgpxP%nV6WW zPJZEA|20(5Ft@Y3&Oj3kj=!z3su)=tZ5o$FQM=n5{JrKw1fcE6K{H zexl2kLSA?@2}Cy^W)zyC@J+_EKcDD5FOOQp)_{&N>>pr^?tYyOK(1gfnRP@{0&M9_ z|EQf9AqA7UH6DohVR~3S!m)R2*$)K}&@?acY%w_27KezEMBWZrzxD$TvF|139`NaORJ z!tjtiO)U((jiDIzMVx=}MFzBUo34P#?amDnIz(k<%YkndL-CVLc5<9qzZdv5Kn!MB$eAP}{!)i`{tCYK1@Ym>07 zmm>}e%3j@QJJR<+{1e!`mbzA2O;-fj+oGyRc1ImjE>;CObdP%VRA)L&D^NS@lXi6n zla?8}Rd!6`OpQu95QvM=6Bt|QH1Mt#$K!mAw7cr3LqdXNmFY3*_4w^WR9yx-t|5Sk z3!f6gv}?)K)qd_TEvqE+rt}yM`MDR$E4d$pT!Gir3|1D~_8hz~!+CyqsrYhA=hsX% z7l)SMVF5}BsXOb^Vu)j<$s7LZ&oo7TZm!Glcc-+@a#xcz1>4h%3kwVvCTX6>brYOD z6q~Hy=$|u6kgkOs@*mvk08zz**%mSnL2=vr8!^t2v}odY3DFlxgIVf{3J!wu(GBi7 z%V~|n*slZuPVD$(*Ri~-c@*l_HqX+6bnQK~u+5}W`AN}-hu;^jyet%FB7Z?kevFQ8 zAI7Q4pR!>)d1=w;s}9&!&IMaO!_02{Y9}t6V~wL)2FiXFs0}>LAWO>oP1=sN7q1s> zYPHjp4sOzwN4DS*=u>CWJj)E(I!ahd0s`=$%pDAo%G#ky_LJ$8mOq58Z2Dw;Ytp7h zjT}%`%Gm+9sr6seX{?@uU&;a!i2DA4>tJ>pF$b9JP-zyC)2U_&M4?m(r8Et6cCYj))G*v1) z$f?G>YSN-kx50T=@F(cX?g1ni&vKSXoIShkh)0=kXO{Yecx9D`oceCcy72}n)_L>3 z4BEa)8P$tng}Fjm5GWSv25r2xHO;t0>D^;3q2SxJFiP@T*)2}n^bpHUVrpctWKi+4 zsjfzN2-GZV-7w22vB!1b{HJXU3(~N)iG{NEf(T~?;bfMf#1N^$TYV*GZ+Bvst1YSH z6B9Sq9PDHivg_+BlP<|$ssF4%M$sQ;l2m%1c1e(Vbz+2u5=}#&V&(mm%WCQaww;^! zZ%8SGi)2V|bR&#QrG_8QOVk=%*`Z|E>x|Y*Y_y9e(5HeW4y$4Arvlm?EA22Gcvp19 zG<%7LLLO~z3mrDaH>4QP7kr%tpM{;IZPQgLqb&hYHXQ8HKM-c+SIlj;EFp43uE5Jz zNGkPeyW?J5jJ&+_W3Vg8%aW!~O$A}P@^P5*_*j^$WAu~+O^JFL7LDiEP$I35eP70~ zZE3Q64czW*Rb2~~r;INw?COqgmxj;{^L3O2H6L}h%X=53AEeuhAmtRh3{d}oI^|`p z$KcL=bobd{GGhKf82?`V!m-$Vw7hSupoD~7`HdrX_wMTs`tFJG0u^Kf`F!_hr3M{~ zWUKq#qGMn53X|E`W^xP8>2no)9Viq?^sFjya&6&EGHLC8mA_({JJ4v8{5CPEh|%r! z-a-Dv;M?JcdA|?y?0o0D#N9r%-!U&V$q<;w!r4XhxD7_FYGlV%+=Cjm969LME32u$ z(iT1=g0C{?jdG8^?1v47d!>YO|lUkjmD9@Se;KW7UJa zf%HCV|FHC=OXkzkxSi?uc%gw#?6|II*Euom_XB4KWL_l_yIA#QldQz%`CAMw#}qy- zo?`UdrtH+Lt)twPG)s1M;YzgvHXV&>aR&xxa+kYhZGPxi648DV2VK?Uj9?9`gZp%w zeiY}jmHdIT0|Im0iT0ivZA7VA%>=2Qyv87q;jsqyOmPUB*cmnj(AU%LG?M~bT2K2a zKjOl0uq4q#j?`}W*cZ=Yf7fkUb}$hB5?pLhG6H;d?49gmR-}M|5VI-GjhkThW}zyR>+{l(P>~Z;jgC z8F02r`u_RBg_NBT=GHp-a7B*`*ZQ`|9x(kr-wBTk`k!Aaof$)nyvgO+`IMA;Cf6i8 zqg>7^DCOzhp53>@wPHoEM_YLDiX0I?6^U*+(-<7LM+kYc`gZ1ggu$6R;Q}#I1-0!U zG_~rYxJ0cI%;Ut@A4WkHFHHtIH_a$6JJW|{4cB=)C*5bEtL!tUSnl*~)=LKMxcqRkAZ97p4s;jl;N> ztB5W82acoI>VfxuEcR}#?Pil?|CC~MzWv4bUt<4(JnKlvElIpx_b98P_k6Enw2LQB z=G#P~wVjTDK6jIILf5@#M+K5A$;`=~0^NN-68TLZc(G?xd^$NGldc@$Y+?E2Yt-}H znfp-+zD25Cbu~F*dB?krblJC2&X&F<=^0d^=>|W~X{phsD_F^*DEYC%p^hJR75=5H z=BkK+LY`VDA&pX%AUS)}s%kkCZSF^3|1ZKQ|0kU{fk*aSsDnh8RzkF+aFZO!uNJ}N zyV*PiYmukS$OcgsSTbWHqVD|q`9m~LFV8~sisVB-F;$cPs2abS__667)PUmQ)vg9k z$_T?=P2xek$&e!aWKY5OduKZ}HcK-MTuiz4qJ^BJj?G7k|AupeqyQMH^e%Zyk>)r9 z~QLfoB`^9(mvflmI-%Kz6zH_P*mZa=>9K|i)29LdJ zNFjY?amk}wjaD9B9Sc^-DQygUrDhx|YU%IIaxG=2yi2C+&8+px&zNle~d+lo)_R&&u3Q?>7=S8Gn^g}*L1ufQqNc8B|haE=h`6Sb;4 z_@R-jp?_*xu3j*r?8N6U1wKi!ZjVcL_HuF+!yH4P2p&S-C!D47bth`0Ug|4VA2XKT z+k{5m{q28+mrw0U+kGhu^9fV$#Z+ba%l6zU>6nuFclz^VC@8aPi%&MTS3*)kSt>5z zl}nMsPA|f9$%6mFARY_6ccSbzu6&S_ng5RIs{K(8Shqc;%W|rkK|yAKIs%3fU6oClT8mq;EWgGU6Uwa zFA-{zjQ{0~Of zty=4i;>l@;QFr>*meBv>-fhXv&MMB*BUM?SDoyD+T-QdoQbA-!($Js(X>%mcB#CgH z;jMQ}4{Q{Nr2HJ7qcJ|$FC_bHK_h0QJl{TtT#_=k%fOg`s5dnZ$FlV5=>;az?ft)= z5uRwmO7HUVi)2m0nQ?pj_ut5=JrD=x7B(0r*d;0fB9t)`KqeHfPk$nBmF5V_2B2TA z9-^MB9$7rQ`D>B7kjqpfzO88fS}jp4f(|2W7aOc^9<1(1){C`Ubc*9P0KK(1^@nKH zH~Sa$O4wVs)wcG)Igv#4JVzgju*Zy#Jcn0t8bJN|+9ONfSlm+-ThZCnE0le@E7dS{zTXg#gA z$k3f+QH4bW?pDYw^1mo$l@lI_)i6T()V(FC%DM0@O|ByMyz;9xZaWyV`YYYfI-kei z{Z`^jk?%qVqV|XUbfDw-WU=UK{bzg;&2$&QlN=2J6!|s^9ZHQ4ZIC3c!tJRZi?4&V zi;*V{RTUe9h@G#uJfzfvp%q^#hyF#^GR)jFmB*J`r4n}Hatcj`o_fb{@0~lE z!?%?Dp^1_mCH){5#0WwDyTA6V{!3v>@q*(-I^*{A4tSANK&jAwASSUln$iZ)bKLpp zpISZtK=iXeoca${jh5bF5qeJZ;39f#oF)j2$kSw+v6ekhjo9!sRU zQt}^A$<5ima}Z#a=e9K)5s)q>O5yWn2ztNwvTape_3t+TFAM{))HIOsF^T#x(DIvG z>|ZtiRw=F``#+pqUsz+zJ8#s`H9Mu|4g!Fh;oTp&h=~b0H?;I+wF?L!N>jSKr*1_Z zQvnf@p8&q~2NiV#j~N{91@Z;3K-8G~XK~3bTMc^liQCGXsRlKT6LxdE2I1psbo(gm zxwA~pYjT{^!Eg>&0^$&kAnA=f>$+7td6%iKow$u-6*Otz6(qH9Z- zFyFlUZ=<%}v13vX?O1^XF#_-)SrTWT#;~Kd)e4w?#f3Cs7WYlhlbCD&yh6#QM-rI} zJDFg4g3Cc}xcNiBjU#vddbXrM+w40;!wf2mbl7;AGXeuhpS0daJr6oU9?DWXgwxDF z2$A?!megJxP1L5O<36h8~+7J0AZ`}8XpP!g$zK_liJKo1z#7Z zHID^~gi`o`%&xf-a<9cw$ zkn_OYz8?reoc063xhy@)uTuDL^wQET(cs$?{uU=t&Qu85vppp~e)(JwN2B9Q+ zIqksWU6Q!B5bFcY6Z>d+w1f6#MLFFMD|)2mZLMTqwih~@b?7jyI)_P3M{+)P1plb9->efd^R)xXf&69^3X;pTUS%X^#%hHB}|=h{tV-`BbP{e~MyJfP_Jkn#)Fa zD$6T}P#oG=P?a`0<`{g7X5k*-;Ui$=;q9f|24m}#F>4_vy5{Ogi+YyCtfcq83j=h89cD8u9)1aI3Ef5)h)eTA(Tn7Y1c4P zQ)smeolm3fsk-E)`7h(qnC}Y>+E!AXB~sXM?>K?j0JAfuPX^3NdHD(|OSuK7cD-~; zP*uT~hZGwfiSQSd?$9$g-3#pKT9oZdpXGqG;JVD1QX*Nex3|QsGvG*V<%2~XIa!x5 zhf9yb^;#wAQB75}J&LZ);U^7`S@sE+okChEae8djfYLr4p}{h%dzj%Ht8NQanZ?%| z>uLp>iiu#8JVG$zSROsWx^I^2&}I9^$xEbynpg4=J$7is4#Nlgojh4 zSAIq9Gp7hLm@^;3$7UCGG1oA}-k3CPLKq!BGxj^K822iv6{SG+MX6K8-r2b|xyGAP z?SPqI!rPlG^!LuDwyRNXJTc&RD1agPKyMj33Ui9it{lRR7B^r*6Ls!igevelR2;u9kno2i=49QA8E_DD) z7lBecV;e34u!-dtE$x8lH-c5#!OW}+(G`a(dvCVyTgHu~#Jb~3926%oJSG1$lrz z8`G1VKMz-~Y$Hn@W(wrZe;l8@#{FTCxS81K-+ICO25APS#hLw$7bB^Q298v(y~|)o zeYn(6dT7bjZs@kxiP2Gquj)*k?%3D$_!IkHi>o&&oU8s?yasX?jF)}SWPIJ|9Z8-- z$LcBhPvyXyhifN98W@$tHske@LEO6nw5apV)t}x*2#u4=&p+N=?ihdQc;RzYEsxOi zQ&zdk_6e0HHu6@b;?AWv#GjW#EzKa|lvA+0w{e3?CIvfsofM^k zga_nC^04gH!n9GZtdGC{;X1Fr!c@FLblyJ!If1m_&C=tctZ>hLV5<`t`dg74l?Bu} zhG9(>Bl3$q=RDm;OgPeyK6?W38p<-nV4 zT{o%#|Nd0e8>+t1kyJ?E8mbfWX{h`C#|EZ%k;}+3{i1%((ytG}o?1+J!O!1Phm(Gd z(k#;t{T&c4wL0H+#ZTh>MEga_tGcBievvM{>ByAb(YQiYf=5mW;pyurg@&5?p~4}M z$e=3bd|=TvMOoLFq>A*W3LG8(ohvbrmqr=TyO7^~kFtAq&nZ&%n8&N5fpu21ZJ#?# zj<*e^3#-~5mdFz_%FYbl?0Z)?ZuHYhaEg>>C_iIsZl3(nrCYv#6Tg-=E3Q*xapT~G zpDAJ@%YHhT#UCeM(G67D|8pAq|BLVaH^T+-ba5^)RqX(HYYE*tD%XaVV#^QC)b2Rf zCZfc8De-V&&+fY|Yu`R*v~xrl(a%pe9^Fk}boy;??o;&oGX0j-1NLQzG{nPV1U_Zo zj7iGOAgaa#?`%>jt;vEWNsiS!!hqQ@U5kFRUIQ~?aJ%Tbr1;IjMiKg_7Bcim(?mhO z2RSos{LA2sXN67s%vj?lD2K238%i)t_kNR7C_ofldnu(YSWx=cGaiM%rXqXyif!+- zVLp~LON%fety0_!>DC-gYkG1drcI3rH%kA9%P~n(#X<-=UjyA~R~O5+w~{m>I}P1? z?PPI25iZeu`awcrA!SKYvs3Yvm-#n)pEH@i>f`O89lhV*eXcXKO|C9{z1y{l_mp4# zHs544*2%80}}9k=YI?yc#{Z zHOnF{oouJ`uyhJ0a78n^ime&OpA!-s817y=#*8={O}^DgdLA0NyG1?1+hK5l-ka2@ z{SNVLg|g;lHE{=F+9A!aVQS2FF8)0*xB2i0+(3z5Q&ZvvLM2#B9l5H{b7h7Khc{rj`TE=<@%iH;4xY=y zx92r8ZqEf_+(=8$=IWh-LcMOu)j3}A5A;_rLRgo^L@D~ce(>I=)Z8k&a3|00t@{m0x6O2SdYlDX${~`dKIuIuF+>ba zDsRVHag~e7+8uUB1zz#GjlsbB`a2c55aj(R8 zoq~^@)L%NZDi%hL8k|6XxrX+b6hX8$L*dB;_mwET5FHzbBShFi@uhE(PzLV{T$5Vs zW=#L`hQQt&h)ErNe};Q_*>AG2$T39KM7TN0qP8Yw!dd$E&_lIW>EAt{b}q368NAW( zM#&1MaLZ0BG$Rp1RbEb$U9Na#K{J+1`ehba6wT3%c)Zpw5iOYJ!|j~^Q^nAp1ZE15 zh)kzZxAvH)>N_FT-pMhV&`0s%6&G-ynWk`kZ0pETgZwYa(Ur?3af3G{oDLqui`?QC ziSat=rM}Q&t7P3aGvM!3s;7KTSS-mP%d=E_xZN@bVLWMU>S;WtZRCX84j&mzQc6Mg zFmi6WLH`aS)#J{cp&6d3&X!TO`n0jLa-11vnzcBZ+feAar7L_59`bI~(d@$-j$K!L z<448rTy)2Uk4KNtV>*!{=4m{|Q7Akw?hg-c1QCKccn@hz%C5mx=_BCmQUuq9jx1MLu_YJ-Zq(a$oIUhVGfW z2l;OPMWbk8RQ}<*^R88e)qVKfyZhl-^%VtUL!BV+H{QyZhFO8+?qqW*bl@3N;N2^k@Q$n@Bi8%&_*N5UNyDJy3dMjmxy*sFQwkA`XdX5ls{nf={`_H^G zpUa!?a?iQ=(btRz)O9Cg!8Fv>t@!sQMv!fu8J?8oeIzZa>C>pkYDao{4WoVU`P#po zt+ZY}X#*h1Inb{Juv`>xQcI)o#S_n;%pyWbh0|$ub&^=$42b5IksefummyS};cFB*r9dOhUaQ9S3=>0I=4Ar)Wgjlfuj%e5$ zG~Y#ERz$icn=3{%cmc~J(WPX)IR76mkr_7dCbqpEbX@UX*hSk3aY+5LC7D5F`o{MA z4&W7d0;*BfHPGxUwEcrG1{HNzMOuBo?2m3j^%k0MqSMhCUDb5;kBJgeQoGH0{YPX2 zoOa-A!{%EQO?3543U;&+`qwd3jo2qWIAj<$yi^{(IFz;o@(w zW%9-*VTTp=FtfZT^zN+GknAIFYsDq0-MM37pv_3wMRzS&aaruCe(ti;RmCc<1hdWN zhgQKs_LD2&gety^L$bW^ub z<%4C`#u&Qs@kuVwPl0@^=*TnrC9QcEp|$aagP61I$cmL)P6F@IyH*<8Ik$L$g?CwVPAgFjp{dOOHKP^UGdehVb1Sl)$`ON}4D{=XPMAvZU!qa5jo& znQ;YjLieoCC{D3Z4gmhh^~7AqKLy=OnW*Z>;)R6OEhOKYTLDa`Fr`NU^Gib#!Z3fNl z`jPAx*_HX*TUV?U(P68-fc|uvKSUBVG$5QN^8z`osla>%+3y>q8Q*biKh~TYH{;Z6 zK0e&AMS#ywYI6opI6AP;e<0vrI^Ma%zR9!>L;L?_U|ST01vPYUNU-+p_P=VMX{OuD z92EQXp(WKx4=n<5_bajRsbZVb&1x_qA|omjv-YO49?PE(}OVO5{ICl6+Vcv_? zufCWEX+N({9re!!$uetSRuWt#X!Pwvo3n``n`pvl*=NRN3;lo;-k|SOZ!Op%Xi@P6 zfAy5zl{g=N#HhkggR)}NlwU}*DDiCw@j(7^eP#3AuS>9u^4iO?C(!B>6-dD>Dgbj(Rw6So&wZDwCxw~4Hcce9-1OJ?BWCpSjv$&1-Wx%D zvl(9=kH}aa@Riem|t{>fqkI7 z+c{|*mY%&m@I2(6cne>ra}#N3Wklkng!JIt;ImC*b8NL`!9nX1!BXquf|K8@YU$hc z@Y>s;`&zjCUk%Rxldu2Ht%F@FiDDV8rjXG?RQwyp)mg(DQXm2B(7bhFM;#hBL!SrL zOvstW)IO|9zjUeh+`WlQ$FDAy$p5Iqwm>A&50eE(96dJCf@~2t&%&}FM32$zRdI;I z72#ENMyHyd*TU+p;OFxD_dG>kcRBgsRAH<{Qq=1c+pjX&#U6v;VqVO%wMD5h6Yg?H zbtF@IxY#jLO`nNvesoZ*lPnsVh z)~HxWYmIDG8RMU2U6J*?NaSH^dhT*p18BwbE{JKq*yxSR+={_Y4OL4zQfuQ>11jzM zKv(MADEUUBijQ;J;g{8~zUE)fGrp>37CiL;KwI-J+fIdY14o)Zi?L)~Mw>@lc(HlJ zW79Q7W81$e9&fV)rthHqU)vQ`V;`H+dI!|lGR=42GhN^w?@C=fHAJ<$2ZJmgWM>+W z7rlMUr3D6bYW{OTPKZ}{d;GED;|os>Ftb7lR(2~xD?UNE+AxE1>&k|joj7hez`6X* zyS*jH-m$iPeBmc(ajuPFIw}-t3qOaS)i2AR7N7B;a(T~>eJNDbA}uI+7XlNEta*V-&iWJa0@;D0|J#lns-?ua5>z| z&?)x{5$4*hz9$sT!sFbmv!$M*b8ldwg&4!2CG6_L@2!vF>Vv$BFLh{IJOp#}OZ`xr zX5o{(5hR1{!b2$+$PyUyMt9ICZ{~vZYLwa8IrFTNi)#la!L|h*7Mbgpq)XE{{S*NI zliPq|l)z`WH`{+@ePLJ8@~M85r_hSwHnwhglwh97dL`%f>(A=TZrFmpvG>^eSHSwG zr(I;iEw>GlNR>RE9jn1swZ3}tBoqx{ls@fj{|f{M@Vznm)BDzX`{WPIwE3>`CShvfJ`E=H$pMtGMHCF-`@An%J7`g-)@y?d%s4oQrt^>ixIj0^z zcF?$UVb#=aI>RhFGHMjk`+-KNWs+X`_m-^DSiwu#aUT1U3D(y8#)znmO1xP^6LM%L z6b%Kl7}eel;UdEoBGP>fQVNWl>VTgR>Q0xNTr0PH8+0zzy`)qdyrF)m?ISGN)Du+> zGml-s=4I!Q9A(!U2D(amaj`PVC*2o1`a+0$?g^1gV@-GXfH3*;-j_P=>l^H{yXU#q z%MwCxVS#I7Ko)dScaqS{IZzEQ;D5!)WnkwpKfPE{8C;uz**V0z=vCpTJbICWf`CN)9J#mq`o&X3oat&Vor1$x)}s8n@gjb@aZf}oz>V=PnB z+LL!M|H<5miJzzt>`RDmRPW+0`qdZ4WUIigMSv1q&L7!&$GOZfwSfR;YxwgXE|+fKJ;8% zM+0lhKoH%j%Jc_eJDO3D^>$b1UGx>A+i!x*uV(;~%>BQqW%(cd{C_78-v2<~!eu{m zgO>^+`X}A!K%Y3wqTQs=pvuWJR)@i}o$;!f*-wS{?#Q)bW(Not84%8Orp3-lLuAu) z%&%ALPTFiFASM69wd`f|P4$V^B19%LB`$6M{&2JfP(q+2PcBYmj0BP#S!hQnNFEwr znyzR%I`uYyy)*drGk|bkW21X3HAH~GpA!h&&EN$Erdq=_&d(M9KsJ+^Xja+ngLB5# ziZrY>%kq6Se0p((WD3-^<$QnUddfnl`TtmUE8kXRfWc>;8v5Zx z3~viq>qnh~P6FT!C@WVIw{t5)s>${B@bL!ajd6b4MTj(`oQsP~ zDzZkKXiz=mB!Gskal(dP6bPh@^TJ&Sv&o^Jp5HeuTy`P7b+2c?kM1Z@oOP7e$6?{v zd;L}I=WcZWka`;q(c{=Orp_Dhdfu7N-u%k`WN{?|107PScP?7BgNi2E^694LHhr+* z@iWLlyS(nZ*j&zcpqVF?DA_qRBwGe|gwF+{l>=m(@^i6Rw;?tMcIs=^3(MyD@EB2p zS#ae9+Q@U?nASGEGd58(pwx+m(XWr1EZY31s9WO*$qFfAC6WkTNf4dxu+@gK^(9aoUV7NH z?Yy+>xf}BIc4I2(H!p3QHS`=zXG_TMceQ0!=N8ic+PkVnlX|{EBTFJ^lLooMFES*i}BTrT@*#5nX1!BC_QOAngt#?I8n+MrTkKr6mkbc|#8CaYN#e z`bD1gJ?|$@(UT(FTq~T$p`INuK}71UKE0jNih|5(S?p#Erbd1wyW_;kM`WV3h=z31 zq+}znW(2$%8-z%H^D8P@Vm|dL?RQ^|e2kURdDhIc z;ueBZIsr}e5_Gf65nJCah)<-ZnvGw|CD-Qta=rLnm`CKWtOq&G_JFNdj>efhO)a9m zR?XOR?C99!wT%-L<3scY3hBgCy+af$S*2 zL62cENW`P5jCL_tLS-OE@NDusm^b=izu0gGm8JCYVU*IQ2i1ubP;hCElrv|ph*760 zTA~|MJ)d+G7U!q_@|(PI5J*bQ&N<2Hc->A=tPhk}68%z!8ctB3zU6XDXV9IMQF40@RI z4>7wp-jq@m;WaBxA8EN@uwuY4>(+IFpbyb|2pY; zKAy$gqjpQp8r2L1=(|#Sa;ip<4~8mm;&<#eb$4x5&-vga?S>x=Had943$=F-+A;E1 zbC2&PLA=m=5HaS=)Txypz0FbrgL((walUUZO+gMIU72Ae<4C)DCZrq?+PgYt?X$n+ zdtO}dn@}mZKXLjV)!E#5R%h|!MA?VMY5cwD{Dt;=!aj}%V@pq-oqZb3Z|j;|A{WR9 zbKDHjhQ6NZDa4-auDswI@Lp^Fhs}qRWeCN<_W=(E^{W&E=L{)D!lYj|?%Mum^F*yv zQ%x)r&<{~!oMAK{O3tH3XXqId+j4IMeeAcC%mC^%(|4u0eyerKy@h495L%33oG>9z z45JTr?`Ft0EtTTWD!ouydSJZgn@K2%4Q1L(MY&Pm7|1fY^)8m0o)HUvf$1=iGUMYQ zj89y1E3e5{-zmK>bDX%ZEm@Q@D8_bS_U{OCzz(NcHn)j!sGV@Q8YAG1Ij(2S5V2=y zw&+U7jcQbtQV9Gm$a9!42BHJ5jA2a4JYEdXaQXDEQ=ObKylFonUHi=YK%hqEN#Cd2 zG2k{fA6uFi!tMN`?&xI1WhGzq zwEE>YlbU7hZQgk{%lpTDFMXjI=xZf&!P6<`nK%tmyRtgCqF>zWl0cw}t@J!P=HOyh z=Giyu465(k5Eq2H?w~JptjGG~}aY_4M%}TUM+$Hx)Oa8+J{6Nu|Pn&?9 zWN`M+C(+uLIe`hOQjIncVhT7hW2nkXEu)W~DO-1lEvMyOJIAt*QYXs7aajr1PADl> zB=JR3cw1MFBej@^=$g$^^7P^?+R8D6(O8QZ>V3+jXUZtkD$MTHq$5dXqx}`uBJ%k4 zYLd&>s&fA$jqtm+J-mqk{8_tnXi9aI2gsV}G8ExtMjtboVz$zPIfl4l&5X!n>tk9q z&o2<$psI0@YUK4JNu_!Et>MWI1CN5ej~v}G%)Y2rHthd84QkmCrD@gZWh~!gOe&l) z%E1lbd`;BeRURmt?dg~eKD4OHxyhfKB7}uucVl`^xs9kqH z;OH5VY;LRvN0+{t&Kk;ub>`L7qTMAj)%&jTzBru#)+aLw6OmbqedsR+1P{!9W-28; zu^iP);BSPVeZzoL+>@!yIY^r{KXd_h04X{p-h@&j#6T}4YB{6kzmCCv^cLPhcn~Qg z`JVeLec|_FGahEesAbz4yZWiy3Y8~GC3w~chL<H64S>>UTrr#e z{Be(3wpgGua0fr>=vMSIS2K1tvGkD*AmoqA=STdXVC(*OV(P{qpX?Um4{*9eFU3#= z5+@*}#(hPIEC&PWKDj2+Mw69~ql2(WW`XDCC7t~l<*;5IPgeL6Z3*||rK71#eOcxL zM;N391~afd8+l;J)q6)Q`=kCvBI zl`EGkm+z)1t1KhCX8!Yur$B+s9l`P4Vk-gm;209 zku#Xf+GogAVr%k918sl&&P@c27F|Ta#2%!&?G~w4kK02D43|3pLe=JS%m~J0;7Im_ zbgE;<6)*<$M9C@-zVF;khtn8}8ejj$%5CJgjNO#dW`ws%iIROb>edAgx0f>Ge(4tW zX)I_JF}O+UO^P9^XFi23oi>I~+B$}bJKRsUBPD((U&*_pqiB_=Rchq-dR)ug&*Yni ztWl!7#T(-_`Lb{Po=P@}H&-WA*so`MzimG#&`~cpY&iRxA#{GVM62P6`SBu!ky?aF z_T)MDz?s|=yYC?nF%oS1Ap>6EVq1w0#Ku9d*C#ejM){=LCK;;)1PYWxJx6gJIsB0o zE_3mOrA?@LX?XC%zMqwC7jTu$@uNs<8u}pftmJ95&_a!vpw89{bu{1aRoKn1^)W9R zuOQOJR8rwM>A;DCu+xrXr?_2=xZaD(Fwo7pN0^h8m4r3KwW*92*r+t+S9IG&VH&!rk7%n%F8Ywm0xt@2>ME#ysPEj!QU@;;8 zXR_rsS(>W)Lh3L*{R0Iez)Wp}1|?T8S8P7_BTtpM-0bwn&ezmj^s*P$2~|9wf_S7n zTmQIS(C|b~TY~*s1fzA+VYkw^dgUqVL=A2?f5|`$v1f<6{A#LFoPv!B0K9Mf8Tv|G z7InHlxi!bVZ4?uW^vQQ&qFeis2Hsi9svSBx^<7&Wp>D;}tQl^g5bVoa$p@Q`WI7*JUG%r@Qpi2>wJ$O(4ITE2iAnE zwqrfFvXE)3M;1G9A6$EDnN4oD3&pmV69kc0$Q0&fF=;X8tS!T*f|rPwc6hY3aY#~S zNLl6N*rODW%r7IkdNKzE1sY%&#-_-z%$fD`Oui>=Lj{Smr3aMrOIHb@#)doHN`^H% zcYb$%j!|f_Z@+OYZu7~eNorhy3jo@C`QEmQQZTHhZibGT7Ld~b;Y}wDbS&INL1$2F zvr6|%u3Qttu8SG4ebri^cvIc6k5tWbMe|~XQSIuR-gY$ z+86^};XzI@;Yg`TfTB&2or=&s6_b}?EtdW5EM${FUF&@hS9$BUd%wyo%j)u!O!PVp zKBQPdD>Eg^KSxQRp7IwCPH{(=Sw_UYviIARz|h%VW}9}7V}^6Pr@sD&HNdiY4U8Gh zm8YbztdZigixsu0q;JbBYqh<<{!r||k#hnyxLhO5EmN2N^v98Q(LQrNJ9xlu5ZU+z zrg7_Y$)L>uT=sthL@K1{-%f9iy|gv?XWz}Y0bDjyT;~;p9)p~#S4I}vL?k*HKKx?D zljJ@Q?Oh?g7r1=hm}_>Bhxl$w56n#8mpIEP^=2&K&Bup zPTgjujQ9va$+seXQlC(HR9>|6%U?VZn6(Wp;(}Gg;1=X7xGffTe!b?`xyzFf?jOlQ zv*!NHd$)m(Z)l(z+7MFsC-3X3k80X7XF+aj&TVM&?X-0^psx{EOXcUR@_~%G1bC*Q zqZevw)*G4tUe5OcR@-}R)dfbjwD|*apje;YViP(t%PdGuxAs5YXIb{q<>tS!Ma6S} z1mM?i`{zb(fSJSC!j;z`CjP;k%9BOLSFk;(zvx;cHe$^o564f=>(U@$*_Cq+^%tPOe4K<|bE* zxK2l%;fd@+RreYAYI8;Ard0%f^7cu~sdt|22I9gPwH0Rg!uscF=aC+5qTI#?(S=ZH zyu=B4d-yRh4?XewdUCVeWNCM$^UN#dcasfc^s8l4dw}GA@`HXDU)^!nU%*_I8{5{K+5HKCt6UrbhWCEj?!pej6QB~d1m070aFSlT<=>$+jV=xoon$hRoZa2FO zw9)yXcV>^V1mQXV26}4=#$8GXEGe7)JhuS6TX$?OO)uhMzrJTZ34&H(ezjRPlo)U@ zvyzZXlc(A5rUfh`1{q)&`+COZY2Y%tql8NyqdgKF*>CY6`EuiI&%7 zxMZ&L*;)~Ku&Y}JgK1CHe>86BVc_uAZL+D?A&U%5Y59L%)`FL;eM!i3q$S$jWX-mf z=aw-tL6RI9rjPCWn_unzTGBgup}7B#+w z4zhI$`8K_#=^l2(_kH7o+PY}d90RT5BqOJ`rDyy~)4jWhg1iVDzS_>GPD85 z5O}U``(Y3-#`F~rif==mAq5!!^7L6|_o|JGlQU&^u%{IX-#YNvXvPmkK+h??FXTWr z&-hSpN{k0C%Jmo`UVTUZr;FrA`tO=Zj*v(a;c=gXvs1;QldsyvJ^Kaa zd4#rhk+0}PihC0KN+3Ok3W7)#A+a)j)oauR0sc{@>a{)}P)4(Dry7=o?_J7s)yuGQ z93Ay0n+Y1CL(ce#9@vAg#SYSMzgFm(_cSlh4lgkGRXJ)O^khM#fq+_GZPcR=GLB^{ zUa>f)@Wn1A$K2lJHC#c#gtP9l2wXmk`sefildH(}fAE!O=6~(w!M^!lMlU{9|1TfX zw#+7wQor0xs?|$9+$a2X~ryN zER$Ur#gS4Nx=$h9K7K>spp%nEZQ@fr#=QD{gZI}0v83PmFTD=Li+|Fo_HBP5Bdw50 zrH434B0DOPvO1sBONX9gMa{Z$734b`Mz`vngMVxrrnvfTTsU76-7Qf74RpSx?DaTV zZdmo8q26`rrf$JyY{|91>3Sk|qr~=`fjr!UGR$)7l~AAHc-P=vOx|wcSge5`Hzp+N zLDzvA-|bH>N#!6%Y{n_(c|Z>8n}yK)gnX7!29yC$2uv0lC%!cd<4*%ac{@wl%8!$s z$|HBud;RsQg4LfN=DIjyKP8&ug-9TZyCh`K*#b7Mw!TE3f~N3F<>=6)8Q5Rehcg5Ry~wx_nkkuT;QNGx46Q}1Pi==I6@j@!o}4| z+?Lw#(vZsL&kkJ3X7p}v7QbLT)jsoNydKk)5bou+*Om9FmO%-54vcB%H45*qRF&zB z`vk+8(lOyzg0gASSCZaGi|?`0BnxqZ}OIZTP_={TR1KLNbGPmKRy&yvkSDf z^EbfZivi**K)?&q56(faC~3}eVABMtUsh@3>AegTn*3W^L{2|u4UC$3hF|hGqlDC| zcnN5F<&o1Bm0PSN9tiDso%HEn_clWE-k$6T9SwrSQU^))5@$w^Zv z3=GRHlMMgfyguds63u^w#X*uj)kTLs@cSXKHe78QfJ;gw=&L(RZ%p4kC7ZkrGV}(U z_nnjs!+-;`H?Q8uBbQV_r`2w-NX z$~DE^DTTpovlZg%PT3Q-J2|*O8^xuCc|USWI4j(<#LaEYZivNkgbxv9G|!M{rmLz^ zzLP^lusSa8`;Wg)gh#)3wS+&?;91o%lb+#vQWv~^oG|xN`QMPdnVWGp! z|KUL0>aiMEZrI{FsKujq0thSCO8;G1-`Ns@-77+hU}4cyVk>c6m+^`OT7JzOXG0AS zrKZjBmExQx=bcmU-j!GXY;f!^nJIy-35oE?mG0`}@n8#iYp_4D)ld2sO)#zw@*SwT zFd%sEBo9~6qI~T|=uS^>f57(At{0_Nc_Wy_Ag%Jrm)^hJmfG(}tnw$1CFLySU$~vr zP*h2f-Svfr2baD!Kx#AZS*CGvQsk88oEWR?h z#BpBCAU!=(^5vk0Y1VF-O?ChJ4`m;*Ft@Z~eABZ_<7#`2+~sE4kjuAvi+rF51{kCXN_4H6puxAqGh6kY3{zmoDmDWmY z3ZzXG;w#O^oZXBhW0BqkNCk#^=5TUgxf5c{nm(Jj0goNH1O1(ejf5k`%YH*q$@h3) zXyonto*s$}-D@vg!9WdsxL~4k#Qc1bg2^$fKsWjN+r~;zMT345Uo&0kx1R&audX(o zb8^!;;h*5OqiyRqQ*uxd@HPf3Ew!s`#jmvc>vm@=Vb-Ji=d7mPLOZ{CkppxG{nn0Js{$hTtD&bHq`D(~ z4E7~cDwZaBf~L7-NW+TEu^@|SRig;^4xOaZcL;xbhFtruj3bWO;9W7NT6pD1Pezfr zqz-lLTIS74y;JW#)CEN>|B&VdZaW%~F{=#1@e=dsqZ}P3)|Mf#2os5i1I+}$h3O3; zj}@_vNXX!F7y^j8aYY?qftWy8w1!4~2T~l<{>+7|^j+*LshrZE(9U0E>nJ$(9=$xR z!yyg%>4e6LmU%jVvBG`N4u(}rU5mPNh{*-(Qga2De9hN3@h$m_OIL=m>p87$p8Gw$M@IP z6^48@?&)~ zS)~80I-XUe)u~|E-dQfAtpI!*=H@vgqSJjWIL92+BPtvV>jz&Rx>hI|r29$owVFPC5cTEU zyY$LOF1G9x`fx4%B9H`f6qtu-uoko!vwSmq7;NkoCdJsA6_2hU?YH6OidpUejbb@~(&pDg%pk<_4ti>E!W z<>1(KqUVmgXZ_v@gKFm3j`f0zLJXKnGOm(ZGLwqRiIgk?aLBjpHqZkJ(JJbQb=wh2 zI$ifVc|zFk*iXk78+FVp3fr?pVdKL z@eqR>oFZqbdDR1+!nCU&KN$#_otCef?bDMDFY{dWh+}1pS^e@%(SUeM7)VvyxScZ; zOH0$@tQKjtRR1Q98frOo4!o`memGT=;%#egFgEh-vCZ&Vgu8u?k!FcmkCjvBPUUJx z)~8t?LeO_kG5a7xmqKV)R--vG-NAyikA1Db(ii$|)}DSNb{!@JthaG_qr=|2{F|%+ z-L$KsZ$qx<-Ms%+g6#K-tQSgNJIGK|9W9xvO58EJEs`x15PmO!LB8+A^MD+^nmI^#Y*OGIxQ+^8koqN$St%6F?{!;x@R=%djSGgQ3 z=X2O&(DvOxrQkrhxzO>jBCXOh?13OaWVgxu-x&-2HyQ{1Kh$cljG%(xHjAXzTDD5^ zaj<9Eix=YrvSMKw!diP=!COb~m!;Qw=uA5bfte}Av-_fX>@oZSezo?ia|CLPfw1=zsTJTU3Xd_OP z0N!T;pP|cBfl^L!2^TlJxAKs4{o)>wuY5DUkM7?W+~e0vkO=oID|~sAT3N=O#P63K zbW(bt@MdtE?Vqsx$s0?^P_WYZ8 z1Qct-ul^uuIf2Sfu3@3$3j z@+bOYX5+k6e(`AaqRHDl+qUT~Pv72kWev6KZWB`D>d7uxI&?#O%%^62Z2U#{O9)VQ zq1(T{(LUoUDhlwYab8ulMa8?i+}n1svJcW43D||V&q{bannlkf<%FNt?l2&f0MY6b zw18KgAY%J({~w`YyWxsy=}n;_A=~)z+Mhk<3tel$jv=z)L4>bJ&-YLE)3)m779qD- zxzj+NJe#7m1b`BX9?m~efe@2-Ml4(j*n40*L*7K`+w1*dEo~M5Y4$3XC@CpVPTd;M z$YSp+`Kxm}VBx2ge5ds6YB^t13@8uM6`t)^x-IrkTGw=h9Q%s12=I#u9n0bm;PQQh2z2%trpascrdy?zZ<> z@Yq{GST(b9uEw!jhnTJ(c|ycdL} z=qpm6p68;G*lWGBH{(BCW*Y;3aNHZ-CkC%CO%FI~y3GcA?@oWSN14Lc{>yFy@@WnN zQJz_OP${Et5Ggy#DOXAn1vAqN8PJ&a^E_`-E!`)yjWbJOzI1=ks0*UlYH+DarK`y|IO<_wN~d_Rcm5;%zvXnTLh3;J?E8T*!A zo1pF!%K9;Xc9`K)l-|?wAYyd%`kG0!({b-v>&J}vRFSyH_+gO!590shaXEV7H&U(j|lau4JL z^GRvA_lh!qk3KXvnB&z=6->YX~6SJFs^;0^?*K#f;p1$UH&C#<5h1(#P1EeK<_q zgv7Ifv1^RP9awKzQLF2(-2^C9V`UR+`siWW+t1#-pJTJ@O)9vh77Y0mOw{6LUOT>a zlN3ueS1}oPa*T%;ct7kLZFCc%_vn3cmK863(pCSl>dEoq z)Gz;W$QjUrs{E_n=fDtwd)q#mZM2(du#fG;D0yyh3VD<1m&YV@NbR|x`8+VuVdJ2< zp$Gg3+elmDvTnYU4idMT?Q&OIzqt-G3Fh(Tq>#*+$IZ{wv_qZn5Ia1qlD#A}$| z?>^Kr+Ac41-8q;l(1wi;EiRyfJtwog8C@+qyEvKW$s)9_NICsPS~2ih z^oB3eLK}{~(Zj48(SZl`%<|@3#2ByZL4oZ_r5?H1sQ`X zbq*~PC~t9qt%`g?!oglhS7V!DqbrmfZOZVQOb3-1N+P5kjkBwqa$$ zQ+jVJ;Rsugz7n&<7AZv(I1C{j{3Rz}Xjn*$z$SiPort7wZSEav6k@Q@@#IP2>TWoQ z4=2Z?l;=2yV<afYQJH4Q zr@)dnERWadW`|k z7yPT(HyLS!7N4u+c0%=~M1RUI>WHgj1F2DTNBd8}&rygw&r^yGz+vu~puWMRREh05M(B8h)76a0B=(KB9MEAsqQ~t+Rppz5 z9ck3ao0!j4X1t+aDjQA8qI8R!z?ibxDQpcOe3HT>_Q7}G;<-6L343LREn4qS(q8~I zyXk-udmr2-c?61-#G>S`rsl+De0Q^D+Q%>_9c zl9X}B+hIO1;%ISE0m5qNoQ6{M;0B1x7_E)NPK9;p>c%T;-0Q10GvDylaLf|-936kO zR1A!e!^7|h17*}?W->4sd0<~me(H_lNU&9g0xhBe+2tNJ+#SX&*)eBIPk9UhVZLW8 z_04*g>Y?8*r|I|xbfrpWu26llO;eIbejIgqU~l4^PHdBJb+5>mkSULVBf+idE>Z)& zzOIa+qz{yE3;SWDae-X)hPd)sa!Y1ACO*JamDei>FCQ*)XE0D1c9qdymG^eUD_5#! zAfWr24Gt9cbVytBj}mI`r=44D$K5;wX~e=N)Ege}a+G<6Qa*Wz(}(_e;{{54enPaK zY35B6zoXiCkve{2^Ck0UdQj*p6IYBUoux0X?CZ#ZpY}Q)BmY$TnkS(KzSv)iG;U&E zk#0{GXk`ePFyvzOj+M=63Xd)oC*Py3>}Oihh`HyOXt_ky>M)D_kfdtWJx~|K^4ga! zEV{)UdBi=KrnMeJAF5_$qf}`SrUG`clG4jRCnX0i?8GyS+?-&>i7EIq+?{jk5Md7% z9`hE}H`;^CV=FT=@(^e!eW;4S;B7`}5$KzryulB_==^V}l5$jiPEJTMLdI$syEOgO zpH;HROO-UyXv8t6@DdC~)DLM9i1*(}!?$d0+*)D>b=myeh|$^00hSH-sx<}7IqJ2WZY9x|09(vCXIP)>e^bfh|p zX92i#2CPMw4iN*c%|gP*-Y?1gY>Oc`Mp7QF8%=TFY!v&y+I!EiCf9CV6elVcFcuI& zAQlh-k*U&KrUeiXB3)`!LL)EzdJ&?uNR1H6M5ZEyGN~a!dP}G; zB*gRht-Y?j*4pcwz1BJFob&B{%^zOMo4gQTo@YE`jQbww$RvqEF}D4c2w~7F zK^?$7%7N7RZC6RxIGJzQovkv;Un@}2RW65m{D%MqI(IQNq@J~E;ZKQ+kDz(pI5Vdt7_^h=o3D8@m&0(P_;5its96Bc%`=V&g1 zY!({0s9TMx9Dsz{231;#`>>{sd^q$Zf2NPJ_cv@jLXxXa1*hN=R9?tafuh)_O}F!S zdRaX2-bxfvMgg20ozTs%p885%JZBDh+=(Q$AP-*HgS{kX*+EVf}L z+wTbX7kpW@xI^7|h3O5T5t`DHBC|tVxgmokL`N|7tJtS-mY5wdal$>=Zc3axmOBxQ*l`!reb)U_M!R;^f!Yft1@2$IBgzcbZ%9Zy$5N~!l zkZShR=ls>BNCq2ero?>2wjoC0Wrt0fAW7!;>q~FvtvZSr(ay|SB^a=&^RL~w=gPUQuRm1FY_UflQ%^lScw>!V_2%BaUd&=tY zxtrUsKgG_wo!*a9Y{|ymin8UHS7fTOXX-eF7nnM$qOdVzVTD4}kiEknRj-IgeIoli z-i5avX;*#rM?~()M3pHV=1S?Ku>8hc_+Yt|sbm!c>!tCLvA6qnk3bISXY4q`oi`vHb!$qBE zHnyT7>s+=(HBtl8x2!J>P$F>`N)5a1>({d^-O?OCJTDtg!gimoG#?Am`av55h_*J* zrAh|t(ED&r)W+dRl45%4T z#H$zRqd+a8@O1AFmKuKV^xmA}v)RWwtH;T6{pmK!>c*?at3&lEpn1Df%x6I&4{``-K}$=VY407Pg81VEj9VR2?WChJf459ZNgLv|NP*_WHJ5O{;15K! zM9mIi0-$#xCO>4CPla&+Z}i(0J(qrZKNvh0$V}4wFD&JBhc>nLrH zW`Tj>sgmozU1QX51U3>THw2$XP5}9VVoO!(#jl4RJtnWt11_x zPAv;b36H(xzuxGa`(gRB^i*j1w;l7BAiV#5)jb;O7oW&vzuZ^?fPRs)baJZ0R{Lg< z3pe0GVmc|sV5n?Gj1W-!%Vur5f2V$Zpl{5Vwzn2c}EyOWC-X0jZqdTR6cMi|NM0aa-@DzgPR4uNTM9y+KC@c$B^ z5Irx$9=p5_KAa<0kp$3GR?%2+X2q!j9=6SbFPq;&rHFH zYy(%#4MH=1NvN%yPXV&1sK0%45`Hy9%qMS70;(cnYB?*qJN^99`98Xv7n&cix$i;) zm*B9=;e~o^TcH%}GGF!g>~`Ux0GSW`B#)o2uP%BScg&MXn|RbKs}uu3%vAaPN8eHy zkJ$Fn;uSqO^7a0&!nZ+3Z_K?K=+LxQglip6tW0o_Unh)h4-M5`+I8GW@AqwN+Z0m3 zRZv1x=A%0zGiWQs_ZNwA>YcdyEw2BM4r@UaU3qH<$>j%&|{7Sw2Pz3oT^pK4J| zxmVAqRG#mfQPO+B$X)W9F9Nge{JaMB?Rx6B5jDis@y-Zb02uWS`wiAFp|*bO$2aB^ zj{IrY2B$Q6Z>O(a=mB8Q6ZYFoZ5VV*A3o+v20Vkc;&LJWM&t_Fc$-e-30OrJnmHSG zn<&((rYHv0uRAb4ZSy}uHb8!y#_ZpI&KLVGK$ngU?42U)&%KTp!y%Hs9EI<|-Zg5I zJr}eqK|}C^U@p#^J+vOPoy|k*O|_;m_VAQY172JV%O$zye)6b1DpqoLEE}{rn;kAe z^qW14m1vwCYk!EZJ-fH(Al8sZ8`OVu2OZ+&f2KB|Z+Xa+ORRjUWu5U`;mW6M=v0cL zQ>XL2Z;6&>wbkfj?IeT`+Ce=r_e}P#vxTOC@}h$^(Pos9g5pmW$W`@dUJaYV6AK#) ze7@+H*P}1S|BB!D)&Ig+Oko{r9I+Gfy20*?&EnmlFJk!8P?X(=3Qj|I!PQ*o^IP&c z)#oLCL-n@Dhuod*ZqU6)m0sv(H>22A$Y}kO39K~Sc?iZL3eNzq!OljIY(W?Yh|HdA zb!aUVio(m!4Q7&jIXV_>@um@X6h|e72g{xAORiZc4P(Cc>y$7PF4>*GJKghg3GL zE7690)}-Y6`Fp7qW9x|+u9gA*1|-Daw}T^ANqw3r$uWx5-NA_;Vx_#)KkMNl2a&j{ z#=%9AuPE?c^tI^ThonMMTla>Z()vL!%U*PePxesg>z=uzF<+Y zZ^0tLfAB5u*zE;ZXzXqf1FycT&#@#ox5zx2}zw#0fl zFT=;ZSIlmt(MzYI78eU2MRRL+8OIgpM%8psc-V-s17i_mSp)%H zA@=MP3-kO)%Q|1W3Yb%LzVNQS(e3T-4Qu495d>xfI@Pnu_UM4X6>R9mswj(|w-sIY zn-;m2Wxq6rkj8`-gW}&a|aw~G6^>9IVKy> zYea@#>471V8mhaT1Y^_sUXNo^((3S*V)Ul#dIU5+j)=nVhJqL{-i16u(-(+34&M(6 zHP+ZQpY}caf|=pf+(*$ULt#HLXd8L6i;T-v<>{!IRCTg2CGf;$bkQmNcAaS}s*3QE zdLJn~ElF4~W)E3&0-Iy+kGG{HN3EmTyur@MHNq|);(bM*TBmeX!Z=23a8_TGKEE16 zt#qEIG@RH5?#?FH$Qu}9>3S6-I*V4flAf~0VcEnr%h~hQEXtl zHrI*(@vfKfuS4%&?%FTc6Z@{_n z)k&{DKPr{45sFA!qK*|(kHMSmcK5e3-uFjNbdN#%eKBmomTssOF(}Tmj^8d|AHqF`-YDehfeZm@!hhHd5MEAD~;{x+2-MqM`y z1EpazIZWK;M9)buGun9o>TuwQtU9DTB|q@-heg#m%w}FAXc9MsBGDzdN6;c3MrdOZL2YkuVM#{pNy@mMoe4WKMVZ*Bq#}kb!tC_u;5+~>qt85Y9uEGiKK(~i!>hgyn;o8XDod>mSFkFb zOd6l}QZ49tnH(Px(4Bp@s;HK$?SDadVEaAvPt`$od7cfb{TNm1!k_RTPTH(T zkZCa*pt~>p3pxU*hwnHBysz&rwNwrg%!StihWG*eIf^qc_H& z=A!-QQ?!SB;s;sI+DkX-qm$ik@q^5_DoYRk=+dgH{Scm0f-5tB^f2Bp-PJt8E z&XhR3cw+KGPD8q}tVPQq1A~LbN|$ivgy%H3L)bb$`T1g&ocZCLY%MLdPSJi{*$Oj8 z@RFPBgj>_!hWo`|CY{sMj<)f%t1E6IElxRW4l&G(wUkQKyJt7DnR*DWN(>l7e_!ln z;qj}plS^rA_OwNjBNS}(wc^8xDCF09*Emhp>5+8PbKSUWWc%8Q0vCNvP z@}-~V@d;!Hv=6T@b(X=~3A}f>hcC^wiD@1irw}ZGq{|lto1Zn0o$#GTblyf>{;d1c zh)&SzTHBn1p}Ji#wZm)&k8iu}x5iVGDl&v2ebNb;!y%sL71q!53s=WiCo4>r7O9zq z_$;6P`nt+qUJ2hd2}}(UOt2Csg1Uj)=GKNeb$BV(BvSk0(W>1|n=zq5cgm+fH=;^pq zG0m%M?}`LAfOO=zJQewXfSTCsDax%mAY?W4qcS%^qXO?$;z^TkmZ-buIkA=Xi(Jae zYC}W3;p>xsyA$mGlJo~*ZT3pwK^=ENj-H2drp}`wDX#y=_4a(jYArpX6TV;cNqjP1 z>3Zv6CE1OR$>6>|VBP8qJ(~H-b=T@#$={K=ODHu%^wG0+fxt2D6fgQiOYKG_BhA*q z!)#%4bSdwJbHnd8R!wmPP{$(4OgmSzn7F;O*8!iJ^;-1KFZXAI+}~-ofogrWcwaT} z8JZGZ!}xS8nr_`kF0FturK!2vSv~QW>AspWR5ae@ap;#&mu*i`mroS8; zvo5IbbYYlu*mrDo5l}CPZORB{%Gy`Y!p_rin=^FEW{Y`D!YB66m*kNs4EHh5gTqNBQQY_Hc_x{%koTxrt&m`viqHL~gKD}T2_%Aa`#?fd18 zfwJPG$zYGn?*f6VNwu%U0w;PDrfUI90Z9k^3bUKHo@xWJE}Pz8vIRoSwGdX$A_c|D z3;CPWIV-~+o8)ymkFi!TCh$=THem%?A3y9)fG82a@)QvKB3gAWuK}*ZbnbqN0#0F? zTdy%!4~FY}!T2AHnomKkjDj^WwlApE7z_g=Q7h2{E{|&m2)1Bf(SIoqFb$Zm4GgjU zv}6{rxF`R_Iv8iP`Lxl#^u-q}hR*u4jZSgKnyoNFy`VsF;q9w4EaT}Msh}0!I}uOG z%W~quo}&vCACerV&3a%3wSX${3bJdQ`S#Vn-knC-;A=qWY-y{@J*9rJwmKqSZ9xap z3Jtsp?CS#6TPIs5fq6p4t(>=)1{<{c_(#zV%It-aC?YP9gLGlf!@P?||MWy1u5%6X zo}9VVLh$lkH|UXlB3D*JiJKI8RTrab^1H@aav31eo{c9&1o??j9*{)&2U>^X#Jzw{ z=f@KXt$W7$pEq(fa58-*WY-UDyNIlso~#dcJx7r5f(xc-Awj+A@Tb)!(>;H zIt8&h8CO}00!?z)7WWU4<8)G0{sDijD>p6I6MNkDiHWORq_i9}R(tDqtYS&b=VjE~ z!6w$p=Pwm{w|Xb`S^1wl%3gg?_&HZL-Azu(UQNNE*f@WaLjc{iDUiirJyrI3HYjj1 z_%CbhJ}OI2?uB2EEd>H%?tiN35DvVMQPz^wQfux#>a6@pQmUoDAg3jGg%07NL6FTOP@dphWT9ZhJ3^ z5TB>C1!11UTWmS`>s-`zjsBS=u^Ebjb%vdSb^_T^Oo3tdf^2B5*x#jO>*ua*lC_a- zo_oC|S4)0TeONCoFf2mRM^9Q?OrbYlX!KCAN#d|eJg5;BR9|lv!gxo5uf_M@sPEI) z=9%=?1*$~XVOSVxXj2!#9=b+_LNj&Q1{+T+y-xvTvj!Fp0Ghqf<8Hm9eB+Zf0A0!U zR3RLz{ZNx??{3I;TyH<(aWy;CohYYnOmd>4 zcDvz5d6K<}zhrr#f`)pN3okKeIH#zmBw(Y^DlMa9Go^0RRjE~D!&3hVG{oMGh^30C zB6oYbAUMbfo-5PaiZ&__00WZ9(DXy7oYtVu9y>*ENe*)BOY>z0$Gm3QiSB?QraC<}{2yvr>m=L1$PgGe~mpafNTppN?KnYMxnN%pXv#ye13DD!lRz>3BO z8+ECINfXGS!?3I}cGX*)CTX6M-S*JMF-Gh4pQQSN!u5xhx0XvCx~I~W7pr}*n42Jk zr7|>qAItsmX6ItIXqtEJX#Y#*dF!mHz4;PXyk#eL=4{>_y6OEPXNWo(2$Y*s0rGL6 zum~c3r2epVmgQ7YnBE#6gYskQBWFP! z_5C~=Kx$6#EOUyr4ZRVt=6^cukEIjV@dF!23 zG~(z>k>AWDjJ@UyN7TW0XK=DvRVg`+9xlhf@_z=D^(J2f&V;Vg`0-9SI(xenq9rFkK)}W*P(BeU0b^eo)a0mEtDjHJsLUSfAX>|0*7SoUv#c_#UvEoFba{~x!Uy}1MR zT04+OWJnXYZONj-Fs>kkDflk%G@%p6wVSELn=k;;FNBGBL6WYZib9O0OsO8narko| zgE$I#CdirbNZmgo@E9f$Oma7$st|%_P*st4sGnyOF~m06gb3p zmlll2f|s_E^fhtEq)5>Kg^2_gUUkrTFv98<&vn2i*ows8p|Y%>FksC$4jCm)5^ZeS z1Z7h*fX?t7Uu?pZ(=qmLj^ob!LYRO2^}-OJRGv!UsUSibtF{eN0F-tEG~i`_HN$Mr z)v5da1Hn2^p8U3MgNR23u8o9D6`yUFkRn6@3`ar?ZP!0Td=RwdN?eIvzQgGpE0Dt3 zL%wkgjy)7iYU5r_uoY@UhUVPkIYK^{hWJMiFb>GOktL^1vMGyGZftIeSIB6L<95fA zcsXggytnmdG2dtHomGg-MAVGs_^bJ!h1U3)!(07{Ds8CHFa!q>SXbNSTQlKv(?Wni z&^dtwREZ__4cMjYV?g*26`=0 z8E^7~IlfHW6hdI0C*cqemOra5es~ad%LU56R#jJC6A^8S@%L7nH52aopd|bFRL#Mw zJECtx18jm6&I-aA>R{V`Y1M)Q7%8qf*zS)^F*>P5<{*^Bg6(K~Gq;slGQRYpgjsLn z@@dKZ^FhA)a_vZa@eCz-&L92Aw_5UEHO1a6eez1@xnCv9t1hDg!^V1_=Np=epZ!!SaTmqa z8yEMWh|9~BRv~jM5eg*9Ns?1>BHM5J${TB4fJ*R8lQ{QJQ73jsgh^TGUa}=zhA*@V7jKXRi_DwJ;NR+R>&e#kSG1Z)`&Ota|7mR^A$EPPA=UFO zJ}oTCr21%1IQFDrhLgI}eY<&b%7O9FTICO83;< z$sJ#15g-)fr#8WH7!Ao&#%U2dxu)R(5f81^PM7M1XwEc9%3;s0!+0(#kp@H*RI01dM&X5*ujK-M%v0#- zV`QsJBuF{lGiU@}8T?bFb-rJsZ2ZtjG8w6?jox^2cg7mEyhwod>-h<%TSH)I#qL+w zpx~GZ2pZ$iUF@tbhWRDrLl5EiCCeYzjkORG`p00bsWzG8T2uMF@DB)=^aPuBouOpg zei!>EPoBIF2BkHC(?;CMSsQx6K))D|k1R0&awl;*!CO;QWWFX@3SI&G7%Ieb!9X~jUqz#7>9(#fvos@$7w%atPYAd;j4y*P4g+9fjm{CkFHI;`w%K& zZk#N#MN*j@poX5Ss%daPs;~>>HMj>^9VH5*dRoysOAu( z>-3mj!J9dup9lx%wDCl-VUD3(@^O2$y<_&@uCo_kE#A8#@pPx;7=1ko65i3-iisFl z=k!OuXXN>x+w%L2o8hFikwozFyep7$F4m|Oe}I#-Zim;etuOoP|M|2_{FkHbk&1_% zzPplI=$DVTCmP?gx$>XVB+uHQsXV|kXkq=;J^1=s$vK`92li1$ zEpxWNu|W+;jg-E96USO-(6;y~Z_Us<%Tz7Qd}T{VzSOYDYQOs?LlF9&$ZdWfLCz=b#qKd_vcZy}?Abwcq2o zztEK*3pR5c>kV}AOJ3Ieeuqs>1H$u1#Vq zHR8!Mf>qD{&{jBQB_zKYI9c`5$6SYWP19$Oy%F{N1!k(Ya9!%5x#R%0_tazL45-ls7{{gk1Pqa>-f%(w{Z_3TOAlg z#+`pO^IhPA$!T*-{Bpb_BWZ-V75sA19peNY;h4L+};)&a*c;kw`K86`;EicQj zh`bqyP}V~jZDsx`AS=8k{fIsU%zm6)>Blxsga(HYWc0k6Q`TQ?XMUU-Co~ZW5Sgda z3BnU_h5wA;O9GV}d15SBPg;6_ipFkR?cs(fZaQ!;cBz!DcaP}yuRXLbGC&Qqe{#?< zH2>7@NOs0COl5;T&#c5feT>f>U3)07Ti~N~F)d*|X*<*6mmkDOcD~;4oT*4Xfd2iK zqWR;;pw^#!7sz6;i+?;dVZ!p482!#g!o<|(`hClM``TcwoXpJmy2_!?K_OFlSaOrh zF;3qLUncdKha@l^JKnB*zou&~MicW^KKaX}QmyelN`{Miuvfv|4-uah<8F0*jlAi# zvb5e09byMIqIOg&`!=COcyi2<`v?i1dbyeD^={<{7~6gePU{P|5TBHTr>>u>tw?o5 zj|)Bc_0#mn;zKBx`~_vM?ijELU4*QDgfA2p#g_7P-y|q*>a70>Qis4+!cQtV1QUp< z!gyw6XK0G~vZ_Wsj5V^~rf2bV1!pqC*N;75k23Q=H8j+6Zsp>&Ce2sh9-MkFjg?2d zWwO$%-CR=R%?hY^B}aR2Mz+>ON;la-OlfS~78!rQAMLAey_lk!UG~^pX(r(HTGYp- zXJB}|PK!y5l2QNKAcM6s;oQaQT&+PK9GG5zINScH2#X(;zH+brI&UI*T`BGPqO_Ch z`%XnI#d%n_|3}=}f9pH2{VDzAo`NPth2lZQPwU3@`bIJPtP55QlQgYj5`2D+j>dQm z$d$zR{|+Wm%9{QYmWn_92jBY-X1(~Y)RM?5Pz>7(3^jXs=^&*b&;DD{O`N5L0k`UR ze{(k8OF_$hc$Yc|;7GrKh*YD?Pp^Cvba`WN12~16T!>$gP32~^ra9!%7t@eN_)IkX z9D#O}{{_DKquni(3W%TQKqV5Xyq%7iCgt-FtSy1`sb|%iD~Vc}1qnZV8Lm6JoWkD; zlH&G|whlPEg<2l9eFS7TjiaEq-64r!#t__LTk;^dgw+F}BiUw6Pc&HZpKm{aCa5-S zZ2O@eEw}M5VCDgmeQg`T4kQznKM|q3i~$<|#TH>~$>qDiL~IDsK&CZ?x0MghTO(dr zrWd-MFx1K8r3~XhlTp}_t4ZiWAbHYr3|?6_{}^TcA=tlo5~R~l-E>7+bs4<}*^Pc7 zNX`#^7dSCRS^)w~KM|(BHHg8Z2%p;>MBmgBVy z81$HaJ#!WoiV#me{-VVDr5wwa?I>n618(^zaS+eOxwpeq@qtwpJJj_gvY3# zyTaq%V#WBEPM$eapY@JMn-8uB>I?DP2s_u1VSseArXtSMt_^DuABA8JukVt0;H zpW-faqFUj?rIn_ik#FG`^J=i?k=}?a zbhXa}hZ?$fY;J5rap4t%wj>@{(VIRdppe+{RStS}ci%I(VYrB9C*RxNvAF%h3hUl;1fu!tZ9AUq~vv zlH>6uNixYsHM`SNs>?L>g1;;i_=%aBXWLGM)f0mfl`O;0x@2WY={mg3NDDu`r)7Hh zcHXeH)bL&I06Z5w0#bRTF*s20uMumH*9J%;y5;tb)K4qCE8Rq z)X>5H+J(imrcNg0T}$w)>MNE#JiILp#1sYC!!^`6x%bcyA3i(Cb&NJ%e7!-#gD=kR z%VjS$r*_gDXZ(PK)AXQoJtYR!y7gdCuQ#r8r#2}XC-<9&c2AA;H|^cUg`fR%jS8$i ztTKkr($lTq$Y7h!YZS^!25wk|6)es7^tfqA6y$#zbQv`r^!?&Da?H2q+rG&@$uC}C#p~q%wVmkKn%2u%%Z%0#Vy1}5w|0H%L;S_T4!_*3)k1Onl`0j_$ z)$toYYTBbSe66pl-PmyBw%0@1rsW zIf?oLob`m;xC`U(BTgk7;ku5Gb|(buP1Khr6-n4G(Q4w}-zNJ}Bt@qbS~HGWuUn7X zzRgAs=Sy_NOLadqSx)M4t*gG*jVt|Rl>z)_G!=$kRc){KgnUcDymFR56OS}1Wc zBk13p8)ROT+7jO$p8+Ou6#jechl5-(69pXsozKcUrzU!Mj&#b?v=CoJqlOma1L?YYb?F}|V^6Ju#xnc8bVP;x%b$y>tp=iZ+e zbKZOz+sv3uh`8Qk7u$c)M{#zphj+URr;#(r?>6NGu~x=^!m8mk-;pC=jbL2p0fQaC zpYpd)HaJl$?m2OoPGfw z)&uNm=0-Hn>c>B?$_^rjR+W#gSXA=qk^5t`a^D?sd{Y~Dyw+Y)WH#mC`@@)>y0&OD(gaoOi0{bS)4XJk`3w zy36GoM2M$>B zzvF90>j;^-AW93{aW%dRbm{-Mq=6i%e}d0IROB{OrB}&6@jElmvxPxU{s@S%)DYry z?bx$iARQA(!|Srcpcs3OVKF;u%ssA>_}oSP1AXym*Xb@*S7xJo{_IyL)GR(EcA~nx zgwSZim%?`v1RLO3`ua1@MiU|if1X2q!E^m>dMgFMsVwRqtPZVaF=g!aX>Wo{vM=CZFh?0JI(HMY)be80$z_S~5+FO7$vrJUoEH0j|}<}YnbZUw!w%^QC6 zoJKn$t&G*Q@XYBoH##P-qiN6{Tu1kMt@TNv-MADJU{S2CDB>}kYnhn6r^WbF7a%3B z{|H^E`B810{^joik3IwBH`y8-VuW(S_K|P@oW&_}Okt3MN>hj(OFoJxm&Zduxxr&0 z7;APz*dtE$xw@0$H0k}#GM6BNMl%(Lmdtj7YGB8H_u*aIS!P@`lX0D1+INvuK{E)8rFyeupc$5h?=`(DkwJU~p_d?=` zVanom6@0%#iI~2f?S;wEM2EZn^7}594sR!(Hg_8S(j9Tb;GANHMnKUOwb`chsomq| zfiH*erRsgfgERc;7k#+=e?P(9=NajsvTXoC`%lb|WHc#&B#OTEu5ke%Wlz5vg0{SU8y(J-Jn`Eqeeq@hy^Q*_oq_NuO5Yw4%RkY5K_c9}{p7(z(+Sk)P4XUK+j-Ncl*7kYPwWOhi9}y0oOUIxyJF<97vG- zN-VpMTbOdOiw&O(?d@G?Y85`BOwGd9L?=0On{34WYIzf}JD2;yOVHix^jI}ZjhKWo zpD&+m(Q2JojPKkZs{9i+{BqIPs9MU;h$;>eI#mAWNsvc@a^>9 zlevFUq_j5VX8})nP*#A8peTu6PvC=-WeoZ6RweIqKrwah@5PkSKi5NQYX+jh+FqUy zQS09bzdNn)pAi1_-3LgpZ1~2HRMOQ1are8xK>Kw!_D{t>?K$n#|7@qKQajS@3|McxN)vBU7m4Vk?htq z*717FZrDCBV5+b8&ilL<_FkGD8^A5}g~im#!5NoUdt=9f@mqO*ALFo{YBLo#dmD2$ z?AG+9IHY+PJE3({8&FjRtvEt)xBK)j01pH(Y${iunVsEs$2}j~py}^mSCP*rNY%*F zuli%|u$K&z8yDSc;&jq7F6TvKCnC=(JK`Hrf>h=`ER|(gjVXd^#@l4106hYCl}QjS zjVi{PuNZNnBJk4CRr_ZZ1AaDt+pIy-oJCK>FVH{N_3nTBs&i)mILW)NIb}=!`tn=0 zO{eZe4>h z*r92L41f>!qC4$(MqOU%;BLWt3PlvKX3NMW`> zrdhz0WT6W8>1M>`+x%84KFv$dl;S1zsv z#mZ)Xs;UjHTHNTJ?RN`SK9}^0(*{7;yU2)9mA;1kb2V|4@v}bs-B{ly&@{+$V4xL5 z)}q37QDYcMboZRsd7_e&Q)dxY&?s_YLFSW2`3Gv;S!VL}Gn8rmQG7cI)^HdC?p5Td zN{p!UY^D^L>~BFHxJ4@(nV}D~yK#J3&iO4Whm0R4Z@IlKdE`!ZFhb{4_!>z`y6S3N z7;!rESU(4d$h_%nmxrT|T?q&>QJqj|Qua5oOmHE8Fjg2!8?_*wGOInyZJ3>}@6DoL z6SXin`7fi46rovV7n*QbcRvfL4sQ+jCvPEQb+z7NU9Cw-Y<5E=&Eb6-vL^nEG4n&j zc}t0rH|X1`YQHLAM;_-lHpt@J7<=#vIFNs^)0)@qrkW5ijGq#Be~4biw*H(Qi8d-# z$Nn7VxuL!l^ghvkw7!23aBJ%+J$ETJGsk~NDGCso^^s0043Za|n zMdqIU(5{{?mF^#;)nW*?j;Q6!+7fkyK5P5goo;$_zl`X1J{67FH8*+^eqiw=M&V?l6RE$#$Hz*iZX1OJJL! zcA-2Z1?_Qu(8L>_U3cYo0dcRI&bE*ujv>3*5Cv}>2NpGyZ!7id5+eUc?}O;poR zwfUAgcYL0>pQ3Q=!d2?vS&;k3 zEuru=Z!aUD>6_~v+y*9DlPik`9gTA5G&Z*Ecvmhs{y4PT_*E})lI=3u*y8Tm< zi8mOFvP#ThT1u72_;lMzk+dHrrQiEDaa>7FH8HEVq+Q7`?-C$TqA$ydV6+|I7PkbUqtc(w({Oz=Fc<6p#y;ar z|K+Y#;(?im6;;WDl7nv#(pHz6mO}Xw5V9C|wNDojTs-Q;GvOS4#d7xcH(t0OJ;()m zUr{(QZ<+L}_2kcDg{1Hk41SRWAx+@NAKWXhHFzm_-Q>GKc_mweUrddzT&w>- zx%~HA{`uDbUH--ubA1WzX?!|k{VM-7Apc(|u%kO5d7%}ZT%iHNjH=Cw%K!AK-`xGr z2LxOe--}q5*^Z~qFW^MJ3luG}Ni-8}`L8f4`d=Md{|cjj zh0%Zb4EF!+Fj@x{qa6Dac()MGgX3;Bd$Kb1=Ic1JD2?8f@zOtgV zEB^Hdx5xkW!T);Ve=U{2Hs@ba1;p{c!r@;B&|fF#|4z9|?{+oy8|cKNm$)at3xHH= zO`=C>rbM29*};7~Y@WKBst9d3Y=<@M$0iu}_`wcAu6qmRjiLxIMYu(c)z0Iea(y1<7uXU%L&E4&Q6jz7PjtpLH1zF&Z`e@w!Si~miIJ# jbMRigx!5o5vY9!PPh|n>@UQ6ge|`PG^z#t>KJb44WlIC` literal 107137 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 diff --git a/Svc/TlmPacketizer/docs/sdd.md b/Svc/TlmPacketizer/docs/sdd.md index 085ea7c5621..28185a46203 100644 --- a/Svc/TlmPacketizer/docs/sdd.md +++ b/Svc/TlmPacketizer/docs/sdd.md @@ -4,7 +4,7 @@ The `Svc::TlmPacketizer` Component is used to store telemetry values written by other components. The values are stored in serialized form. TlmPacketizer differs from `Svc::TlmChan` in that it stores telemetry in defined packets instead of streaming the updates as they come. The defined packets are passed in as a table to the `setPacketList()` public method. When telemetry updates are passed to the component, they are placed at the offset in a packet buffer defined by the table. When the `run()` port is called, all the defined packets are sent to the output port with the most recent . This is meant to replace `Svc::TlmCham` for use cases where a more compact packet format is desired. The disadvantage is that all channels are pushed whether or not they have been updated. -Uses can change the individual rates at which groups per port instance are outputted upon a `run()` port call. Groups are configured with a MIN number of `run()` invocations to emit updated telemetry, and a MAX number of `run()` invocations that shall output a packet if it exceeds MAX. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. +Uses can change the individual rates at which groups per group instance are outputted upon a `run()` port call. Groups are configured with a MIN number of `run()` invocations to emit updated telemetry, and a MAX number of `run()` invocations that shall output a packet if it exceeds MAX. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. ## 2. Requirements @@ -31,13 +31,21 @@ The `Svc::TlmPacketizer` component has the following component diagram: #### 3.1.2 Ports -The `Svc::TlmChan` component uses the following port types: +The `Svc::TlmPacketizer` component uses the following port types: Port Data Type | Name | Direction | Kind | Usage -------------- | ---- | --------- | ---- | ----- [`Svc::Sched`](../../Sched/docs/sdd.md) | Run | Input | Asynchronous | Execute a cycle to write changed telemetry channels [`Fw::Tlm`](../../../Fw/Tlm/docs/sdd.md) | TlmRecv | Input | Synchronous Input | Update a telemetry channel [`Fw::Com`](../../../Fw/Com/docs/sdd.md) | PktSend | Output | n/a | Write a set of packets with updated telemetry +`Svc::EnableSection` | controlIn | Input | Asynchronous | Enable / Disable sections of telemetry groups + +#### 3.1.3 Terminology +Telemetry Point: An emitted value. +Telemetry Channel: A tagged type and identifier for emitting telemetry points. +Telemetry Packets: A group of telemetry channels. +Telemetry Group / Level: An identifier for a telemetry packet used to determine which packets get transmitted. +Telemetry Section: A group of telemetry groups. #### 3.2 Functional Description @@ -45,7 +53,9 @@ The `Svc::TlmPacketizer` component has an input port `TlmRecv` that receives cha The implementation uses a hashing function to find the location of telemetry channels that is tuned in the configuration file `TlmPacketizerImplCfg.hpp`. See section 3.5 for description. -When a call to the `Run()` interface is called, the packet writes are locked and all the packets are copied to a second set of packets. Once the copy is complete, the packets writes are unlocked. The destination packet set gets updated with the current time tag and are sent out the `pktSend()` port. +When a call to the `Run()` interface is called, the packet writes are locked and all the packets are copied to a second set of packets. Once the copy is complete, the packets writes are unlocked. The destination packet set gets updated with the current time tag and are sent out the `pktSend` port. + +Each telemetry group, depending on section and group configurations, are sent out on several indices of the `pktSend` port. As an example, a packet with group 1 with a TlmPacketizer configuration of 3 sections (0, 1, and 2), and 3 groups (0 and 3 inclusive), will be sent on 3 indices: 1, 5, and 9. Port invocations to `controlIn` or the command, `ENABLE_SECTION` are used to enable / disable each section, supporting downstream components that rely on different samplings of groups of telemetry. Each group instance is separately sampled from each other, allowing for individual rates per section and group. ### 3.3 Scenarios @@ -53,9 +63,27 @@ When a call to the `Run()` interface is called, the packet writes are locked and #### 3.3.2 Configuring Telemetry Group Rates Per Port -The `Svc::TlmPacketizer` is configurable to have multiple `PktSend` ports using the fpp constant, `MAX_CONFIGURABLE_TLMPACKETIZER_PORTS`. Doing so allows each packet group have different configurations for each `PktSend` output port. +The `Svc::TlmPacketizer` is configurable to have multiple `PktSend` group outputs using the fpp constant, `MAX_CONFIGURABLE_TLMPACKETIZER_GROUP`. Doing so allows each packet group have different configurations for each `PktSend` output section. + +`PktSend` output is ordered by [section][group], where within each section telemetry groups (inclusive) are emitted. Each group within each section are separately sampled, and have their own configuration rates and logic independent from each other. + +Each group is configured with min/max delta parameters, as well as a logic gate determining its output rate behavior. Min Delta is the least number of `Run()` invocations between successive packets before an updated packet is allowed to be sent. Updated packets will not be sent until their counter reaches Min Delta. This mitigates against telemetry spam and allows users to configure faster updated and sent telemetry compared to updates that don't require to be as frequently updated. Max Delta is the maximum number of `Run()` invocations between successive packets. Upon reaching this counter, the packet would be sent, regardless of change. + +Each telemetry group per section is configured with a `RateLogic` parameter: +* `SILENCED`: Packet will never be sent +* `EVERY_MAX`: Packet will be sent every Max Delta intervals +* `ON_CHANGE_MIN`: Packet will only be sent on updates at at least Min Delta intervals. +* `ON_CHANGE_MIN_OR_EVERY_MAX`: Packet will be sent on updates at at least Min Delta intervals, or at most Max Delta intervals. + +Groups are configured using the `SET_GROUP_DELTAS` command. +#### 3.3.3 Switching Telemetry Sections +`Svc::TlmPacketizer` also has a configurable constant `NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS` that allows separately streamed outputs of packets of the same group. This is useful for downstream components that would handle different telemetry groups at different rates. Examples of this configuration includes live telemetry throughput, detailed sim reconstruction, or critical data operations. +Telemetry sections are enabled and disabled upon spacecraft state transitions through `controlIn()` port invocations or via operator commands. A section and group pair must be both enabled to emit a telemetry packet. +* `ENABLE_SECTION` / `controlIn()`: Enable / Disable telemetry sections. +* `ENABLE_GROUP`: Within a section, Enable / Disable a telemetry group. +* `FORCE_GROUP`: If set to Enabled, telemetry of the chosen group in a section will be emitted regardless if the section or group within the section is disabled. ### 3.4 State diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 845c254425c..89ca33a767a 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -972,7 +972,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->clearHistory(); // Group 2 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 4, 12); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 2, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_OR_EVERY_MAX, 4, 12); this->component.doDispatch(); this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); @@ -981,7 +981,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->clearHistory(); // Group 3 - this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 3, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_AND_EVERY_MAX, 0, 7); + this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 3, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN_OR_EVERY_MAX, 0, 7); this->component.doDispatch(); this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); From 38a2bb946b3d95051b762bf918fc26295f9a54ac Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Wed, 28 Jan 2026 16:27:18 -0600 Subject: [PATCH 11/16] Spell and Formatting --- Svc/TlmPacketizer/TlmPacketizer.cpp | 57 ++++++------- Svc/TlmPacketizer/TlmPacketizer.fpp | 2 +- Svc/TlmPacketizer/TlmPacketizer.hpp | 34 ++++---- .../test/ut/TlmPacketizerTester.cpp | 85 ++++++++++--------- .../test/ut/TlmPacketizerTester.hpp | 6 +- 5 files changed, 91 insertions(+), 93 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index a98981694d5..fdbc1eb3152 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -111,17 +111,17 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, if (packetList.list[pktEntry]->level > this->m_maxLevel) { this->m_maxLevel = packetList.list[pktEntry]->level; } - } // end packet list - + // save start level this->m_startLevel = startLevel; // enable / disable appropriate groups for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { - this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].enabled = + group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } } @@ -380,24 +380,22 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // push all updated packet buffers for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { FwChanIdType entryGroup = this->m_sendBuffers[pkt].level; - - // Iterate through output prioritys - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) - { - FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + entryGroup); - if (not this->isConnected_PktSend_OutputPort(outIndex)) - { + + // Iterate through output priority + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { + FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + + static_cast(entryGroup)); + if (not this->isConnected_PktSend_OutputPort(outIndex)) { continue; } PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; - if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF){ + if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) { pktEntryFlags.prevSentCounter++; - } + } if (entryGroup <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP and not this->m_sendBuffers[pkt].requested) { GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; - // If a packet in the group has been updated since last sent on section if (pktEntryFlags.updateFlag) { @@ -405,7 +403,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::EVERY_MAX) { pktEntryFlags.updateFlag = false; - // If counter is less than delta min + // If counter is less than delta min } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { // Keep flag true but do not send. continue; @@ -413,15 +411,14 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } // If Delta Max is configured and the send counter for this section and packet is greater than Delta Max - if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and - pktEntryFlags.prevSentCounter >= entryGroupConfig.max) - { + if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and + pktEntryFlags.prevSentCounter >= entryGroupConfig.max) { // Signal an update if counter exceeded max counts for current section pktEntryFlags.updateFlag = true; } - - if (not ((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED) or + + if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or + entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED) or entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) { pktEntryFlags.updateFlag = false; } @@ -435,7 +432,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // serialize time into time offset in packet Fw::ExternalSerializeBuffer buff( &this->m_sendBuffers[pkt] - .buffer.getBuffAddr()[sizeof(FwPacketDescriptorType) + sizeof(FwTlmPacketizeIdType)], + .buffer.getBuffAddr()[sizeof(FwPacketDescriptorType) + sizeof(FwTlmPacketizeIdType)], Fw::Time::SERIALIZED_SIZE); Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); @@ -472,7 +469,8 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c } for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { - this->m_groupConfigs[section][group].enabled = group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].enabled = + group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } } this->tlmWrite_SendLevel(level); @@ -506,9 +504,9 @@ void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cm } void TlmPacketizer ::ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, - U32 cmdSeq, - FwIndexType section, - Fw::Enabled enable) { + U32 cmdSeq, + FwIndexType section, + Fw::Enabled enable) { if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; @@ -522,13 +520,13 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if ((0 <= section and section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) or + if ((0 <= section and section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } this->m_groupConfigs[section][tlmGroup].enabled = enable; - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, @@ -541,7 +539,7 @@ void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, return; } this->m_groupConfigs[section][tlmGroup].forceEnabled = enable; - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, @@ -551,8 +549,7 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or - tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index ce6aeeb74df..1f5c9ab3bff 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -160,7 +160,7 @@ module Svc { format "Could not find packet ID {}" event SectionUnconfigurable( - ection: FwIndexType @< The Section + section: FwIndexType @< The Section enable: Fw.Enabled @< Attempted Configuration ) \ severity warning low \ diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index dd810b1b52e..3099fc36988 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -11,17 +11,16 @@ #ifndef TlmPacketizer_HPP #define TlmPacketizer_HPP +#include "Fw/Types/EnabledEnumAc.hpp" #include "Os/Mutex.hpp" #include "Svc/TlmPacketizer/TlmPacketizerComponentAc.hpp" #include "Svc/TlmPacketizer/TlmPacketizerTypes.hpp" #include "config/TlmPacketizerCfg.hpp" -#include "Fw/Types/EnabledEnumAc.hpp" namespace Svc { class TlmPacketizer final : public TlmPacketizerComponentBase { - - friend class TlmPacketizerTester; + friend class TlmPacketizerTester; public: // ---------------------------------------------------------------------- @@ -96,11 +95,11 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { ) override; //! Handler implementation for command ENABLE_SECTION - void ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Section to configure - Fw::Enabled enable //!< Active Sending Group - ) override; + void ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwIndexType section, //!< Section to configure + Fw::Enabled enable //!< Active Sending Group + ) override; //! Handler implementation for command ENABLE_GROUP //! @@ -112,14 +111,13 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { Fw::Enabled enable //!< Active Sending Group ) override; - //! Handler implementation for command FORCE_GROUP void FORCE_GROUP_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq, //!< The command sequence number - FwIndexType section, //!< Section to configure - FwChanIdType tlmGroup, //!< Group Level - Fw::Enabled enable //!< Active Sending Group - ) override; + U32 cmdSeq, //!< The command sequence number + FwIndexType section, //!< Section to configure + FwChanIdType tlmGroup, //!< Group Level + Fw::Enabled enable //!< Active Sending Group + ) override; //! Handler implementation for command SET_GROUP_DELTAS void SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, //!< The opcode @@ -187,13 +185,13 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType m_startLevel; //!< initial level for sending packets FwChanIdType m_maxLevel; //!< maximum level in all packets - + struct GroupConfig { Fw::Enabled enabled = Fw::Enabled::ENABLED; Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; - U32 min = 0; //!< Default for Backwards Compatible Behavior - U32 max = 0; //!< Default for Backwards Compatible Behavior + U32 min = 0; //!< Default for Backwards Compatible Behavior + U32 max = 0; //!< Default for Backwards Compatible Behavior } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; @@ -201,7 +199,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { struct PktSendCounters { U32 prevSentCounter = 0xFFFFFFFF; bool updateFlag = false; - } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; + } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; } // end namespace Svc diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 89ca33a767a..2c56ed73d5e 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -956,15 +956,15 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->m_primaryTestLock = false; Fw::Time time; Fw::TlmBuffer buffer; - + // Set level to high to enable all levels this->sendCmd_SET_LEVEL(0, 0, 10); - this->component.doDispatch(); - + this->component.doDispatch(); + // Group 1 this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 3, 3); this->component.doDispatch(); - + // Send every 5 on port 1 this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 1, Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN, 2, 2); this->component.doDispatch(); @@ -977,7 +977,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 1, 2, Svc::TlmPacketizer_RateLogic::SILENCED, 0, 0); this->component.doDispatch(); - + this->clearHistory(); // Group 3 @@ -987,18 +987,18 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); this->component.doDispatch(); - this->sendCmd_ENABLE_SECTION(0, 0, 2, Fw::Enabled::ENABLED); // Disable telemetry on section 2 + this->sendCmd_ENABLE_SECTION(0, 0, 2, Fw::Enabled::ENABLED); // Disable telemetry on section 2 this->component.doDispatch(); // Disable output on section 2 via port invocation this->invoke_to_controlIn(0, 2, Fw::Enabled::DISABLED); this->component.doDispatch(); - + this->clearHistory(); /* Configuration: - Section 0 Gorup 1: 3, 15 MIN 3 + Section 0 Group 1: 3, 15 MIN 3 Section 1 Group 1: 2, 14 MIN 2 Section 0 Group 2: 1, 4, 13, 16. MIN 4, MAX 12 Section 1 Group 3: 0, 7, 12, 18. MIN 0, MAX 7 @@ -1007,18 +1007,22 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { */ /* - Music Testing! - + clang-format off + T=0 Tests Updates of packets 1,2, and 4 for Groups 1,2, and 4. Updated Packets are emitted. - T=1 Tests Updates of packets 1,2, and 3. Packet 3 is emitted, while Packet 2 is not due to < MIN (Each packet has their own counter) + T=1 Tests Updates of packets 1,2, and 3. + Packet 3 is emitted, while Packet 2 is not due to < MIN (Each packet has their own counter) T=2 Packet 1 emits after passing MIN (configured for port 1, group 1, updated at T=1) T=3 Packet 1 emits after passing MIN (configured for port 0, group 1, updated at T=1) T=4 Packet 2 emits after passing MIN (Received update at T=1) - T=4 Test updates packet 2 for group 2. This tests updating a packet when time = MIN, and should be emitted. (Packet 2 and 3 have their own counters) + T=4 Test updates packet 2 for group 2. + This tests updating a packet when time = MIN, and should be emitted. (Packet 2 and 3 have their own counters) T=6 Packet 4 emits on port 1 after passing MAX (configured for port 1, group 3). T=7 Packet 4 emits on port 0 after passing MAX, even if it had received no updates. - T=12 Tests updating packets 1, 2, and 4. Packet 4 on is emitted since it is updated after MIN and before MAX. Packets 1 and 2 are updated after MIN and may also be at MAX, which is then emitted. + T=12 Tests updating packets 1, 2, and 4. + Packet 4 on is emitted since it is updated after MIN and before MAX. + Packets 1 and 2 are updated after MIN and may also be at MAX, which is then emitted. Packet Updates 1,2,4 1,2,3 1,2,4 @@ -1038,9 +1042,10 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { Note: Packets 2 and 3 are updated and have their own independent counters! Expected Output: 5 1 1 1 1 0 1 1 0 0 0 0 5 + + clang-format on */ - // 1st Channel (Pkt 1, 2, 4) buffer.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); @@ -1079,12 +1084,12 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // run scheduler port to send packets // T = 0 this->invoke_to_Run(0, 0); - + this->component.doDispatch(); ASSERT_FROM_PORT_HISTORY_SIZE(5); ASSERT_from_PktSend_SIZE(5); - + // Packet Location Indices (Checking proper Section, Group) ASSERT_EQ(this->m_portOutInvokes[0][1], 1); ASSERT_EQ(this->m_portOutInvokes[1][1], 1); @@ -1120,7 +1125,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); ASSERT_from_PktSend(2, comBuff, static_cast(0)); - + // Pkt 4 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1132,10 +1137,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_from_PktSend(3, comBuff, static_cast(0)); ASSERT_from_PktSend(4, comBuff, static_cast(0)); - - this->clearHistory(); - + this->clearHistory(); // 2nd Channel (Pkt 1) buffer.resetSer(); @@ -1152,7 +1155,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(11))); this->invoke_to_TlmRecv(0, 67, time, buffer); - // T = 1 this->invoke_to_Run(0, 0); this->component.doDispatch(); @@ -1185,7 +1187,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); - + // Packet Location Indices (Checking proper Section, Group) ASSERT_EQ(this->m_portOutInvokes[0][1], 1); ASSERT_EQ(this->m_portOutInvokes[1][1], 2); @@ -1206,7 +1208,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // Pkt 1 on Port 1 ASSERT_from_PktSend(0, comBuff, static_cast(0)); - + this->clearHistory(); // T = 3 @@ -1233,11 +1235,10 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // T = 4 this->invoke_to_Run(0, 0); this->component.doDispatch(); - + ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); - // Packet Location Indices (Checking proper Section, Group) ASSERT_EQ(this->m_portOutInvokes[0][1], 2); ASSERT_EQ(this->m_portOutInvokes[1][1], 2); @@ -1264,7 +1265,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // T = 5 this->invoke_to_Run(0, 0); this->component.doDispatch(); - + // Not expecting any packets ASSERT_FROM_PORT_HISTORY_SIZE(0); ASSERT_from_PktSend_SIZE(0); @@ -1272,7 +1273,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // T = 6 this->invoke_to_Run(0, 0); this->component.doDispatch(); - + ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); @@ -1296,11 +1297,11 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_from_PktSend(0, comBuff, static_cast(0)); this->clearHistory(); - + // T = 7 this->invoke_to_Run(0, 0); this->component.doDispatch(); - + ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); @@ -1321,7 +1322,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { for (U8 trial = 8; trial < 12; trial++) { this->invoke_to_Run(0, 0); this->component.doDispatch(); - + ASSERT_FROM_PORT_HISTORY_SIZE(0); ASSERT_from_PktSend_SIZE(0); @@ -1339,11 +1340,11 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->invoke_to_TlmRecv(0, 10, time, buffer); this->clearHistory(); - + // T = 12 this->invoke_to_Run(0, 0); this->component.doDispatch(); - + ASSERT_FROM_PORT_HISTORY_SIZE(5); ASSERT_from_PktSend_SIZE(5); @@ -1365,8 +1366,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); // Port 0 - ASSERT_from_PktSend(1, comBuff, static_cast(0)); // Port 1 + ASSERT_from_PktSend(0, comBuff, static_cast(0)); // Port 0 + ASSERT_from_PktSend(1, comBuff, static_cast(0)); // Port 1 // Pkt 2 comBuff.resetSer(); @@ -1379,8 +1380,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_from_PktSend(2, comBuff, static_cast(0)); // Port 0 - + ASSERT_from_PktSend(2, comBuff, static_cast(0)); // Port 0 + // Pkt 4 comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -1390,8 +1391,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(111))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); - ASSERT_from_PktSend(3, comBuff, static_cast(0)); // Port 0 - ASSERT_from_PktSend(4, comBuff, static_cast(0)); // Port 1 + ASSERT_from_PktSend(3, comBuff, static_cast(0)); // Port 0 + ASSERT_from_PktSend(4, comBuff, static_cast(0)); // Port 1 } //! Configure telemetry enable logic @@ -1465,7 +1466,7 @@ void TlmPacketizerTester ::advancedControlGroupTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(1); ASSERT_from_PktSend_SIZE(1); this->clearHistory(); - + // Disable group, but keep force group command active this->sendCmd_ENABLE_GROUP(0, 0, 0, 1, Fw::Enabled::DISABLED); this->component.doDispatch(); @@ -1492,7 +1493,8 @@ void TlmPacketizerTester ::advancedControlGroupTests(void) { // ---------------------------------------------------------------------- void TlmPacketizerTester ::from_PktSend_handler(const FwIndexType portNum, Fw::ComBuffer& data, U32 context) { - this->m_portOutInvokes[portNum / (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)][portNum % (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)]++; + this->m_portOutInvokes[portNum / (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)] + [portNum % (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)]++; if (this->m_primaryTestLock && portNum > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP * 1) { return; } @@ -1548,7 +1550,8 @@ void TlmPacketizerTester ::connectPorts() { // TlmGet this->connect_to_TlmGet(0, this->component.get_TlmGet_InputPort(0)); - for (FwIndexType index = 0; index < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1); index++) { + for (FwIndexType index = 0; + index < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1); index++) { this->component.set_PktSend_OutputPort(index, this->get_from_PktSend(index)); } diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp index 0844f0fca0f..1a28e75a667 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.hpp @@ -11,9 +11,9 @@ #ifndef TESTER_HPP #define TESTER_HPP +#include #include "Svc/TlmPacketizer/TlmPacketizer.hpp" #include "TlmPacketizerGTestBase.hpp" -#include namespace Svc { @@ -124,7 +124,7 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { //! Initialize components //! void initComponents(void); - + //! Reset Counter //! void resetCounter(void); @@ -140,7 +140,7 @@ class TlmPacketizerTester : public TlmPacketizerGTestBase { Fw::Time m_testTime; //!< store test time for packets - bool m_primaryTestLock{true}; //! Lock limited to entries from port 0 PktSend + bool m_primaryTestLock{true}; //! Lock limited to entries from port 0 PktSend U8 m_portOutInvokes[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; // Svc::Queue m_portCalls{}; }; From 29ea42e8d2bc4a2171477ec9193ca7118c92469f Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Thu, 29 Jan 2026 20:21:32 -0600 Subject: [PATCH 12/16] Refactoring Tlm Logic --- Svc/TlmPacketizer/TlmPacketizer.cpp | 138 ++++++++++-------- Svc/TlmPacketizer/TlmPacketizer.fpp | 4 +- Svc/TlmPacketizer/TlmPacketizer.hpp | 8 +- .../test/ut/TlmPacketizerTester.cpp | 15 +- 4 files changed, 95 insertions(+), 70 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index fdbc1eb3152..524a43779a5 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -47,7 +47,7 @@ TlmPacketizer ::TlmPacketizer(const char* const compName) this->m_sendBuffers[buffer].updated = false; } - // clear enabled sections + // enable sections for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { this->m_sectionEnabled[section] = Fw::Enabled::ENABLED; } @@ -113,15 +113,31 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, } } // end packet list - - // save start level - this->m_startLevel = startLevel; + FW_ASSERT(this->m_maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, this->m_maxLevel); // enable / disable appropriate groups - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { - this->m_groupConfigs[section][group].enabled = - group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + Fw::Enabled groupEnabled = group <= startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + TlmPacketizer_RateLogic startRateLogic; + + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { + this->m_groupConfigs[section][group].enabled = groupEnabled; + + switch (PACKET_UPDATE_MODE) { + case PACKET_UPDATE_ON_CHANGE: + startRateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; + break; + case PACKET_UPDATE_ALWAYS: + this->m_packetFlags[section][group].updateFlag = UpdateFlag::PAST; + [[fallthrough]]; // Intentional Fallthrough (Both are configured for EVERY_MAX) + case PACKET_UPDATE_AFTER_FIRST_CHANGE: + startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; + break; + default: + FW_ASSERT(0, PACKET_UPDATE_MODE); + break; + } + this->m_groupConfigs[section][group].rateLogic = startRateLogic; } } @@ -352,28 +368,9 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { this->m_lock.lock(); // copy buffers from fill side to send side for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { - if ((this->m_fillBuffers[pkt].updated) or (this->m_fillBuffers[pkt].requested)) { - this->m_sendBuffers[pkt] = this->m_fillBuffers[pkt]; - if (PACKET_UPDATE_ON_CHANGE == PACKET_UPDATE_MODE) { - this->m_fillBuffers[pkt].updated = false; - } - this->m_fillBuffers[pkt].requested = false; - // PACKET_UPDATE_AFTER_FIRST_CHANGE will be this case - updated flag will not be cleared - } else if ((PACKET_UPDATE_ALWAYS == PACKET_UPDATE_MODE) and - (this->m_fillBuffers[pkt].level <= this->m_startLevel)) { - this->m_sendBuffers[pkt] = this->m_fillBuffers[pkt]; - this->m_sendBuffers[pkt].updated = true; - } else { - this->m_sendBuffers[pkt].updated = false; - } - - // Update per port group flags - if (this->m_sendBuffers[pkt].updated == true) { - for (FwIndexType port = 0; port < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; port++) { - this->m_packetFlags[port][pkt].updateFlag = true; - } - this->m_sendBuffers[pkt].updated = false; - } + this->m_sendBuffers[pkt] = this->m_fillBuffers[pkt]; + this->m_fillBuffers[pkt].updated = false; + this->m_fillBuffers[pkt].requested = false; } this->m_lock.unLock(); @@ -383,52 +380,66 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Iterate through output priority for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + - static_cast(entryGroup)); - if (not this->isConnected_PktSend_OutputPort(outIndex)) { - continue; + if (this->m_sendBuffers[pkt].updated or this->m_sendBuffers[pkt].requested) { + this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::NEW; } + bool sendOutFlag = false; + + FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + + static_cast(entryGroup)); PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; + GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; + + /* Base conditions for sending + 1. Output port is connected + 2. The Section and Group in Section is enabled OR the Group in Section is force enabled + 3. The rate logic is not SILENCED. + 4. The packet has data (marked updated in the past or new) + */ + if (not this->isConnected_PktSend_OutputPort(outIndex)) continue; + if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or + entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED)) continue; + if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) continue; + if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data + + // Update Counter, prevent overflow if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) { pktEntryFlags.prevSentCounter++; } + + /* + 1. Packet has been updated + 2. Group Logic includes checking MIN + 3. Packet sent counter passed MIN + */ + if (pktEntryFlags.updateFlag == UpdateFlag::NEW and + entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::EVERY_MAX and + pktEntryFlags.prevSentCounter >= entryGroupConfig.min) { + sendOutFlag = true; + } - if (entryGroup <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP and not this->m_sendBuffers[pkt].requested) { - GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; - - // If a packet in the group has been updated since last sent on section - if (pktEntryFlags.updateFlag) { - // If delta min is disabled (Disable on change, Only send on Delta Max) - if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::EVERY_MAX) { - pktEntryFlags.updateFlag = false; - - // If counter is less than delta min - } else if (pktEntryFlags.prevSentCounter < entryGroupConfig.min) { - // Keep flag true but do not send. - continue; - } - } - - // If Delta Max is configured and the send counter for this section and packet is greater than Delta Max - if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and - pktEntryFlags.prevSentCounter >= entryGroupConfig.max) { - // Signal an update if counter exceeded max counts for current section - pktEntryFlags.updateFlag = true; - } + /* + 1. Group Logic includes checking MAX + 2. Packet set counter is at MAX + */ + if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and + pktEntryFlags.prevSentCounter >= entryGroupConfig.max) { + sendOutFlag = true; + } - if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED) or - entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) { - pktEntryFlags.updateFlag = false; - } + // Packet Requested + if (this->m_sendBuffers[pkt].requested) { + sendOutFlag = true; } + + printf("PKT %d GROUP %d SECTION %d UPDATE %d FLAG %d\n", pkt + 1, entryGroup, section, pktEntryFlags.updateFlag, sendOutFlag); // Send under the following conditions: // 1. Packet received updates and it has been past delta min counts since last packet (min enabled) // 2. Packet has passed delta max counts since last packet (max enabled) // With the above, the group must be either enabled or force enabled. // 3. If the packet was requested. - if (pktEntryFlags.updateFlag) { + if (sendOutFlag) { // serialize time into time offset in packet Fw::ExternalSerializeBuffer buff( &this->m_sendBuffers[pkt] @@ -438,9 +449,10 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); this->PktSend_out(outIndex, this->m_sendBuffers[pkt].buffer, 0); pktEntryFlags.prevSentCounter = 0; - pktEntryFlags.updateFlag = false; + pktEntryFlags.updateFlag = UpdateFlag::PAST; } } + this->m_sendBuffers[pkt].updated = false; this->m_sendBuffers[pkt].requested = false; } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 1f5c9ab3bff..9e0ace9bf67 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -13,13 +13,15 @@ module Svc { ON_CHANGE_MIN_OR_EVERY_MAX, } + constant NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS = MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1 + # ---------------------------------------------------------------------- # General ports # ---------------------------------------------------------------------- @ Packet send port - output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1)] Fw.Com + output port PktSend: [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS * NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] Fw.Com async input port controlIn: EnableSection diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 3099fc36988..e40a41d928a 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -196,9 +196,15 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; + enum UpdateFlag : U8 { + NEVER_UPDATED = 0, + PAST = 1, + NEW = 2, + }; + struct PktSendCounters { U32 prevSentCounter = 0xFFFFFFFF; - bool updateFlag = false; + UpdateFlag updateFlag = UpdateFlag::NEVER_UPDATED; } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 2c56ed73d5e..4156ccd4684 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -987,9 +987,6 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 3, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 6); this->component.doDispatch(); - this->sendCmd_ENABLE_SECTION(0, 0, 2, Fw::Enabled::ENABLED); // Disable telemetry on section 2 - this->component.doDispatch(); - // Disable output on section 2 via port invocation this->invoke_to_controlIn(0, 2, Fw::Enabled::DISABLED); this->component.doDispatch(); @@ -1403,12 +1400,20 @@ void TlmPacketizerTester ::advancedControlGroupTests(void) { Fw::Time time; Fw::TlmBuffer buffer; - // ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); - // this->invoke_to_TlmRecv(0, 10, time, buffer); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, buffer.serializeFrom(static_cast(1))); + this->invoke_to_TlmRecv(0, 10, time, buffer); this->sendCmd_SET_LEVEL(0, 0, 1); this->component.doDispatch(); + this->invoke_to_Run(0, 0); + this->component.doDispatch(); + + // Default ON_CHANGE Behavior + ASSERT_FROM_PORT_HISTORY_SIZE(3); + ASSERT_from_PktSend_SIZE(3); + this->clearHistory(); + // Send a packet every time the port is invoked. this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 0); this->component.doDispatch(); From 015c0f9c8462a323a099769531df59f4426f36d4 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Thu, 29 Jan 2026 21:20:12 -0600 Subject: [PATCH 13/16] Fixes --- Svc/TlmPacketizer/TlmPacketizer.cpp | 41 +++++++++---------- Svc/TlmPacketizer/TlmPacketizer.fpp | 23 ++++++----- Svc/TlmPacketizer/TlmPacketizer.hpp | 15 +++---- Svc/TlmPacketizer/docs/sdd.md | 12 +++--- .../test/ut/TlmPacketizerTester.cpp | 6 +-- 5 files changed, 46 insertions(+), 51 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 524a43779a5..18cc608db67 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -20,7 +20,7 @@ namespace Svc { // ---------------------------------------------------------------------- TlmPacketizer ::TlmPacketizer(const char* const compName) - : TlmPacketizerComponentBase(compName), m_numPackets(0), m_configured(false), m_startLevel(0), m_maxLevel(0) { + : TlmPacketizerComponentBase(compName), m_numPackets(0), m_configured(false) { // clear slot pointers for (FwChanIdType entry = 0; entry < TLMPACKETIZER_NUM_TLM_HASH_SLOTS; entry++) { this->m_tlmEntries.slots[entry] = nullptr; @@ -43,7 +43,6 @@ TlmPacketizer ::TlmPacketizer(const char* const compName) // clear packet buffers for (FwChanIdType buffer = 0; buffer < MAX_PACKETIZER_PACKETS; buffer++) { this->m_fillBuffers[buffer].updated = false; - this->m_fillBuffers[buffer].requested = false; this->m_sendBuffers[buffer].updated = false; } @@ -63,6 +62,7 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, FW_ASSERT(packetList.numEntries <= MAX_PACKETIZER_PACKETS, static_cast(packetList.numEntries)); // validate packet sizes against maximum com buffer size and populate hash // table + FwChanIdType maxLevel = 0; for (FwChanIdType pktEntry = 0; pktEntry < packetList.numEntries; pktEntry++) { // Initial size is packetized telemetry descriptor + size of time tag + sizeof packet ID FwSizeType packetLen = @@ -108,12 +108,12 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, // save level this->m_fillBuffers[pktEntry].level = packetList.list[pktEntry]->level; // store max level - if (packetList.list[pktEntry]->level > this->m_maxLevel) { - this->m_maxLevel = packetList.list[pktEntry]->level; + if (packetList.list[pktEntry]->level > maxLevel) { + maxLevel = packetList.list[pktEntry]->level; } } // end packet list - FW_ASSERT(this->m_maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, this->m_maxLevel); + FW_ASSERT(maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, maxLevel); // enable / disable appropriate groups for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { @@ -370,7 +370,6 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { this->m_sendBuffers[pkt] = this->m_fillBuffers[pkt]; this->m_fillBuffers[pkt].updated = false; - this->m_fillBuffers[pkt].requested = false; } this->m_lock.unLock(); @@ -378,15 +377,15 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { for (FwChanIdType pkt = 0; pkt < this->m_numPackets; pkt++) { FwChanIdType entryGroup = this->m_sendBuffers[pkt].level; - // Iterate through output priority + // Iterate through output sections for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - if (this->m_sendBuffers[pkt].updated or this->m_sendBuffers[pkt].requested) { + if (this->m_sendBuffers[pkt].updated) { this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::NEW; } bool sendOutFlag = false; - FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + + const FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + static_cast(entryGroup)); PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; @@ -403,7 +402,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) continue; if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data - // Update Counter, prevent overflow + // Update Counter, prevent overflow. if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) { pktEntryFlags.prevSentCounter++; } @@ -429,11 +428,10 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } // Packet Requested - if (this->m_sendBuffers[pkt].requested) { + if (pktEntryFlags.updateFlag == UpdateFlag::REQUESTED) { sendOutFlag = true; } - printf("PKT %d GROUP %d SECTION %d UPDATE %d FLAG %d\n", pkt + 1, entryGroup, section, pktEntryFlags.updateFlag, sendOutFlag); // Send under the following conditions: // 1. Packet received updates and it has been past delta min counts since last packet (min enabled) // 2. Packet has passed delta max counts since last packet (max enabled) @@ -453,7 +451,6 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { } } this->m_sendBuffers[pkt].updated = false; - this->m_sendBuffers[pkt].requested = false; } } @@ -475,14 +472,13 @@ void TlmPacketizer ::pingIn_handler(const FwIndexType portNum, U32 key) { // ---------------------------------------------------------------------- void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 cmdSeq, FwChanIdType level) { - this->m_startLevel = level; - if (level > this->m_maxLevel) { - this->log_WARNING_LO_MaxLevelExceed(level, this->m_maxLevel); + if (level > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + this->log_WARNING_LO_MaxLevelExceed(level, MAX_CONFIGURABLE_TLMPACKETIZER_GROUP); } for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { this->m_groupConfigs[section][group].enabled = - group <= this->m_startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + group <= level ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } } this->tlmWrite_SendLevel(level); @@ -490,15 +486,16 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cmdSeq, U32 id) { +void TlmPacketizer ::SEND_PKT_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 id, FwIndexType section) { FwChanIdType pkt = 0; for (pkt = 0; pkt < this->m_numPackets; pkt++) { if (this->m_fillBuffers[pkt].id == id) { this->m_lock.lock(); this->m_fillBuffers[pkt].updated = true; this->m_fillBuffers[pkt].latestTime = this->getTime(); - this->m_fillBuffers[pkt].requested = true; this->m_lock.unLock(); + + this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::REQUESTED; this->log_ACTIVITY_LO_PacketSent(id); break; @@ -519,7 +516,7 @@ void TlmPacketizer ::ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwIndexType section, Fw::Enabled enable) { - if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) { + if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } @@ -546,7 +543,7 @@ void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } @@ -561,7 +558,7 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 9e0ace9bf67..b4484671762 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -77,37 +77,38 @@ module Svc { @ Force a packet to be sent async command SEND_PKT( - $id: U32 @< The packet ID + $id: U32 @< The packet ID + section: FwIndexType @< Section to emit packet ) \ opcode 1 @ Enable / disable telemetry of a group on a section async command ENABLE_SECTION( - section: FwIndexType @< section grouping to configure - enable: Fw.Enabled @< Active Sending Group + section: FwIndexType @< Section grouping to configure + enable: Fw.Enabled @< Section enabled or disabled ) \ opcode 2 @ Enable / disable telemetry of a group on a section async command ENABLE_GROUP( - section: FwIndexType @< section grouping to configure - tlmGroup: FwChanIdType @< Group Level - enable: Fw.Enabled @< Active Sending Group + section: FwIndexType @< Section grouping to configure + tlmGroup: FwChanIdType @< Group Identifier + enable: Fw.Enabled @< Section enabled or disabled ) \ opcode 3 @ Force telemetering a group on a section, even if disabled async command FORCE_GROUP( - section: FwIndexType @< section grouping - tlmGroup: FwChanIdType @< Group Level - enable: Fw.Enabled @< Active Sending Group + section: FwIndexType @< Section grouping + tlmGroup: FwChanIdType @< Group Identifier + enable: Fw.Enabled @< Section enabled or disabled ) \ opcode 4 @ Set Min and Max Deltas between successive packets async command SET_GROUP_DELTAS( - section: FwIndexType @< section grouping - tlmGroup: FwChanIdType @< Group Level + section: FwIndexType @< Section grouping + tlmGroup: FwChanIdType @< Group Identifier rateLogic: RateLogic @< Rate Logic minDelta: U32 maxDelta: U32 diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index e40a41d928a..00550b886b8 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -91,7 +91,8 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Force a packet to be sent void SEND_PKT_cmdHandler(const FwOpcodeType opCode, /*!< The opcode*/ const U32 cmdSeq, /*!< The command sequence number*/ - U32 id /*!< The packet ID*/ + U32 id, /*!< The packet ID*/ + FwIndexType section /*!< Section to emit packet*/ ) override; //! Handler implementation for command ENABLE_SECTION @@ -139,7 +140,6 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType id; //!< channel id FwChanIdType level; //!< channel level bool updated; //!< if packet had any updates during last cycle - bool requested; //!< if the packet was requested with SEND_PKT in the last cycle }; // buffers for filling with telemetry @@ -170,7 +170,6 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { FwChanIdType doHash(FwChanIdType id); Os::Mutex m_lock; //!< used to lock access to packet buffers - Os::Mutex m_lock_param; //!< used to lock access to parameters bool m_configured; //!< indicates a table has been passed and packets configured @@ -183,9 +182,8 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { TlmEntry* findBucket(FwChanIdType id); - FwChanIdType m_startLevel; //!< initial level for sending packets - FwChanIdType m_maxLevel; //!< maximum level in all packets - + Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; + struct GroupConfig { Fw::Enabled enabled = Fw::Enabled::ENABLED; Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; @@ -194,16 +192,15 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { U32 max = 0; //!< Default for Backwards Compatible Behavior } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; - Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; - enum UpdateFlag : U8 { NEVER_UPDATED = 0, PAST = 1, NEW = 2, + REQUESTED = 3, }; struct PktSendCounters { - U32 prevSentCounter = 0xFFFFFFFF; + U32 prevSentCounter = 0xFFFFFFFF; // Prevent Start up spam UpdateFlag updateFlag = UpdateFlag::NEVER_UPDATED; } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; diff --git a/Svc/TlmPacketizer/docs/sdd.md b/Svc/TlmPacketizer/docs/sdd.md index 28185a46203..0d2f5a8c518 100644 --- a/Svc/TlmPacketizer/docs/sdd.md +++ b/Svc/TlmPacketizer/docs/sdd.md @@ -4,7 +4,7 @@ The `Svc::TlmPacketizer` Component is used to store telemetry values written by other components. The values are stored in serialized form. TlmPacketizer differs from `Svc::TlmChan` in that it stores telemetry in defined packets instead of streaming the updates as they come. The defined packets are passed in as a table to the `setPacketList()` public method. When telemetry updates are passed to the component, they are placed at the offset in a packet buffer defined by the table. When the `run()` port is called, all the defined packets are sent to the output port with the most recent . This is meant to replace `Svc::TlmCham` for use cases where a more compact packet format is desired. The disadvantage is that all channels are pushed whether or not they have been updated. -Uses can change the individual rates at which groups per group instance are outputted upon a `run()` port call. Groups are configured with a MIN number of `run()` invocations to emit updated telemetry, and a MAX number of `run()` invocations that shall output a packet if it exceeds MAX. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. +Uses can change the individual rates at which groups per group instance are outputted upon a `run()` sched tick. Each group on each output section has independantly configurable telemetry resampling rates. Packets can be sent on change with a rate limiting enforced minimum number of ticks between updates. Or packets can be sent with at a guaranteed rate of a maximum number of ticks between updates. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. ## 2. Requirements @@ -45,7 +45,7 @@ Telemetry Point: An emitted value. Telemetry Channel: A tagged type and identifier for emitting telemetry points. Telemetry Packets: A group of telemetry channels. Telemetry Group / Level: An identifier for a telemetry packet used to determine which packets get transmitted. -Telemetry Section: A group of telemetry groups. +Telemetry Section: A resampling of telemetry groups. Each Section typically is destined for a different destination (e.g. realtime downlink vs store & forward) #### 3.2 Functional Description @@ -55,7 +55,7 @@ The implementation uses a hashing function to find the location of telemetry cha When a call to the `Run()` interface is called, the packet writes are locked and all the packets are copied to a second set of packets. Once the copy is complete, the packets writes are unlocked. The destination packet set gets updated with the current time tag and are sent out the `pktSend` port. -Each telemetry group, depending on section and group configurations, are sent out on several indices of the `pktSend` port. As an example, a packet with group 1 with a TlmPacketizer configuration of 3 sections (0, 1, and 2), and 3 groups (0 and 3 inclusive), will be sent on 3 indices: 1, 5, and 9. Port invocations to `controlIn` or the command, `ENABLE_SECTION` are used to enable / disable each section, supporting downstream components that rely on different samplings of groups of telemetry. Each group instance is separately sampled from each other, allowing for individual rates per section and group. +Each telemetry group, depending on section and group configurations, are sent out on several indices of the `pktSend` port. Since each section holds a continuous range of ports, a packet with group 1 with a TlmPacketizer configuration of 3 sections (0, 1, and 2), and a max group of 3 (0 and 3 inclusive), will be sent on 3 indices: 1, 5, and 9. Port invocations to `controlIn` or the command, `ENABLE_SECTION` are used to enable / disable each section, supporting downstream components that rely on different samplings of groups of telemetry. Each group instance is separately sampled from each other, allowing for individual rates per section and group. ### 3.3 Scenarios @@ -65,9 +65,9 @@ Each telemetry group, depending on section and group configurations, are sent ou The `Svc::TlmPacketizer` is configurable to have multiple `PktSend` group outputs using the fpp constant, `MAX_CONFIGURABLE_TLMPACKETIZER_GROUP`. Doing so allows each packet group have different configurations for each `PktSend` output section. -`PktSend` output is ordered by [section][group], where within each section telemetry groups (inclusive) are emitted. Each group within each section are separately sampled, and have their own configuration rates and logic independent from each other. +`PktSend` output is ordered by `[section][group]`, where within each section telemetry groups are emitted. Each group within each section are separately sampled, and have their own configuration rates and logic independent from each other. -Each group is configured with min/max delta parameters, as well as a logic gate determining its output rate behavior. Min Delta is the least number of `Run()` invocations between successive packets before an updated packet is allowed to be sent. Updated packets will not be sent until their counter reaches Min Delta. This mitigates against telemetry spam and allows users to configure faster updated and sent telemetry compared to updates that don't require to be as frequently updated. Max Delta is the maximum number of `Run()` invocations between successive packets. Upon reaching this counter, the packet would be sent, regardless of change. +Each group is configured with min/max delta parameters, as well as a logic gate determining its output rate behavior. Deltas are counters of sched ticks invoked through the `run()` port. Min Delta is the least number of `Run()` invocations between successive packets before a packet with an updated channel is allowed to be sent. Updated packets will not be sent until their counter reaches Min Delta. This mitigates against telemetry spam while allowing users to benefit from asyncrhonously updating channels (e.g. those that don't occupy a set schedule). Max Delta is the maximum number of `Run()` invocations between successive packets. Upon reaching this counter, the packet would be sent, regardless of change. Each telemetry group per section is configured with a `RateLogic` parameter: * `SILENCED`: Packet will never be sent @@ -78,7 +78,7 @@ Each telemetry group per section is configured with a `RateLogic` parameter: Groups are configured using the `SET_GROUP_DELTAS` command. #### 3.3.3 Switching Telemetry Sections -`Svc::TlmPacketizer` also has a configurable constant `NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS` that allows separately streamed outputs of packets of the same group. This is useful for downstream components that would handle different telemetry groups at different rates. Examples of this configuration includes live telemetry throughput, detailed sim reconstruction, or critical data operations. +`Svc::TlmPacketizer` also has a configurable constant `NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS` that allows separately streamed outputs of packets of the same group. This is useful for downstream components that would handle different telemetry groups at different rates. Examples of this configuration includes live telemetry throughput, detailed sim reconstruction, or critical data operations. Telemetry sections are enabled and disabled upon spacecraft state transitions through `controlIn()` port invocations or via operator commands. A section and group pair must be both enabled to emit a telemetry packet. * `ENABLE_SECTION` / `controlIn()`: Enable / Disable telemetry sections. diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 4156ccd4684..7472c243766 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -690,7 +690,7 @@ void TlmPacketizerTester ::sendManualPacketTest() { ASSERT_from_PktSend_SIZE(0); // send command to manually send a packet - this->sendCmd_SEND_PKT(0, 12, 4); + this->sendCmd_SEND_PKT(0, 12, 4, 0); this->component.doDispatch(); ASSERT_EVENTS_SIZE(1); ASSERT_EVENTS_PacketSent(0, 4); @@ -712,7 +712,7 @@ void TlmPacketizerTester ::sendManualPacketTest() { // send command to manually send a packet this->clearHistory(); - this->sendCmd_SEND_PKT(0, 12, 8); + this->sendCmd_SEND_PKT(0, 12, 8, 0); this->component.doDispatch(); ASSERT_EVENTS_SIZE(1); ASSERT_EVENTS_PacketSent(0, 8); @@ -727,7 +727,7 @@ void TlmPacketizerTester ::sendManualPacketTest() { // Try to send invalid packet // send command to manually send a packet this->clearHistory(); - this->sendCmd_SEND_PKT(0, 12, 20); + this->sendCmd_SEND_PKT(0, 12, 20, 0); this->component.doDispatch(); ASSERT_EVENTS_SIZE(1); ASSERT_EVENTS_PacketNotFound(0, 20); From 27a1d053bbe8d14018f76a602e323fc71b5732ad Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Fri, 30 Jan 2026 14:40:35 -0600 Subject: [PATCH 14/16] Fixes --- Svc/TlmPacketizer/TlmPacketizer.cpp | 53 +++--- Svc/TlmPacketizer/TlmPacketizer.fpp | 29 +++- Svc/TlmPacketizer/TlmPacketizer.hpp | 8 +- .../test/ut/TlmPacketizerTester.cpp | 160 ++++++++++-------- default/config/TlmPacketizerCfg.fpp | 4 +- 5 files changed, 141 insertions(+), 113 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 18cc608db67..eca7a4aa68c 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -113,10 +113,10 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, } } // end packet list - FW_ASSERT(maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, maxLevel); + FW_ASSERT(maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, static_cast(maxLevel)); // enable / disable appropriate groups - for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { Fw::Enabled groupEnabled = group <= startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; TlmPacketizer_RateLogic startRateLogic; @@ -129,7 +129,8 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, break; case PACKET_UPDATE_ALWAYS: this->m_packetFlags[section][group].updateFlag = UpdateFlag::PAST; - [[fallthrough]]; // Intentional Fallthrough (Both are configured for EVERY_MAX) + startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; + break; case PACKET_UPDATE_AFTER_FIRST_CHANGE: startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; break; @@ -358,11 +359,6 @@ Fw::TlmValid TlmPacketizer ::TlmGet_handler(FwIndexType portNum, //!< The port void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { FW_ASSERT(this->m_configured); - // Only write packets if connected - if (not this->isConnected_PktSend_OutputPort(0)) { - return; - } - // lock mutex long enough to modify active telemetry buffer // so the data can be read without worrying about updates this->m_lock.lock(); @@ -379,38 +375,46 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Iterate through output sections for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - if (this->m_sendBuffers[pkt].updated) { + // Packet is updated and not REQUESTED (Prevent Downgrading) + if (this->m_sendBuffers[pkt].updated and this->m_packetFlags[section][pkt].updateFlag != UpdateFlag::REQUESTED) { this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::NEW; } bool sendOutFlag = false; - const FwIndexType outIndex = static_cast(section * (MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1) + + const FwIndexType outIndex = static_cast(section * NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS + static_cast(entryGroup)); PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; /* Base conditions for sending 1. Output port is connected - 2. The Section and Group in Section is enabled OR the Group in Section is force enabled - 3. The rate logic is not SILENCED. - 4. The packet has data (marked updated in the past or new) + 2. The packet was requested (Override Checks). + + If the packet wasn't requested: + 3. The Section and Group in Section is enabled OR the Group in Section is force enabled + 4. The rate logic is not SILENCED. + 5. The packet has data (marked updated in the past or new) */ if (not this->isConnected_PktSend_OutputPort(outIndex)) continue; - if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED)) continue; - if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) continue; - if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data + if (pktEntryFlags.updateFlag == UpdateFlag::REQUESTED) { + sendOutFlag = true; + } else { + if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or + entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED)) continue; + if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) continue; + if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data + } // Update Counter, prevent overflow. - if (pktEntryFlags.prevSentCounter < 0xFFFFFFFF) { + if (pktEntryFlags.prevSentCounter < std::numeric_limits::max()) { pktEntryFlags.prevSentCounter++; } /* 1. Packet has been updated 2. Group Logic includes checking MIN - 3. Packet sent counter passed MIN + 3. Packet sent counter at MIN */ if (pktEntryFlags.updateFlag == UpdateFlag::NEW and entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::EVERY_MAX and @@ -427,11 +431,6 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { sendOutFlag = true; } - // Packet Requested - if (pktEntryFlags.updateFlag == UpdateFlag::REQUESTED) { - sendOutFlag = true; - } - // Send under the following conditions: // 1. Packet received updates and it has been past delta min counts since last packet (min enabled) // 2. Packet has passed delta max counts since last packet (max enabled) @@ -445,7 +444,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { Fw::Time::SERIALIZED_SIZE); Fw::SerializeStatus stat = buff.serializeFrom(this->m_sendBuffers[pkt].latestTime); FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, stat); - this->PktSend_out(outIndex, this->m_sendBuffers[pkt].buffer, 0); + this->PktSend_out(outIndex, this->m_sendBuffers[pkt].buffer, pktEntryFlags.prevSentCounter); pktEntryFlags.prevSentCounter = 0; pktEntryFlags.updateFlag = UpdateFlag::PAST; } @@ -476,7 +475,7 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c this->log_WARNING_LO_MaxLevelExceed(level, MAX_CONFIGURABLE_TLMPACKETIZER_GROUP); } for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - for (FwChanIdType group = 0; group <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP; group++) { + for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { this->m_groupConfigs[section][group].enabled = group <= level ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; } @@ -486,7 +485,7 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TlmPacketizer ::SEND_PKT_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 id, FwIndexType section) { +void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cmdSeq, const U32 id, const FwIndexType section) { FwChanIdType pkt = 0; for (pkt = 0; pkt < this->m_numPackets; pkt++) { if (this->m_fillBuffers[pkt].id == id) { diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index b4484671762..636ca17bd75 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -3,7 +3,6 @@ module Svc { section: FwIndexType @< Section to enable (Primary, Secondary, etc...) enabled: Fw.Enabled @< Enable / Disable Section ) - @ A component for storing telemetry active component TlmPacketizer { enum RateLogic { @@ -12,10 +11,24 @@ module Svc { ON_CHANGE_MIN, ON_CHANGE_MIN_OR_EVERY_MAX, } - - constant NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS = MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1 - + # struct GroupConfig { + # enabled: Fw.Enabled, + # forceEnabled: Fw.Enabled, + # rateLogic: RateLogic, + # min: U32, + # max: U32 + # } default { + # enabled = Fw.Enabled.ENABLED, + # forceEnabled = Fw.Enabled.DISABLED, + # rateLogic = RateLogic.ON_CHANGE_MIN, + # min = 0, + # max = 0, + # } + + # array GroupConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] GroupConfig + # array SectionConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS] GroupConfigs + # ---------------------------------------------------------------------- # General ports # ---------------------------------------------------------------------- @@ -110,8 +123,8 @@ module Svc { section: FwIndexType @< Section grouping tlmGroup: FwChanIdType @< Group Identifier rateLogic: RateLogic @< Rate Logic - minDelta: U32 - maxDelta: U32 + minDelta: U32 @< Minimum Sched Ticks to send packets on updates + maxDelta: U32 @< Maximum Sched Ticks to send packets ) \ opcode 5 @@ -175,7 +188,9 @@ module Svc { # ---------------------------------------------------------------------- @ Telemetry send level - telemetry SendLevel: FwChanIdType id 0 + telemetry SendLevel: FwChanIdType id 0 + # telemetry GroupConfigs: SectionConfigs id 0 + } } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 00550b886b8..221eccd382d 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -91,8 +91,8 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { //! Force a packet to be sent void SEND_PKT_cmdHandler(const FwOpcodeType opCode, /*!< The opcode*/ const U32 cmdSeq, /*!< The command sequence number*/ - U32 id, /*!< The packet ID*/ - FwIndexType section /*!< Section to emit packet*/ + const U32 id, /*!< The packet ID*/ + const FwIndexType section /*!< Section to emit packet*/ ) override; //! Handler implementation for command ENABLE_SECTION @@ -190,7 +190,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; U32 min = 0; //!< Default for Backwards Compatible Behavior U32 max = 0; //!< Default for Backwards Compatible Behavior - } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1]{}; + } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{}; enum UpdateFlag : U8 { NEVER_UPDATED = 0, @@ -200,7 +200,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { }; struct PktSendCounters { - U32 prevSentCounter = 0xFFFFFFFF; // Prevent Start up spam + U32 prevSentCounter = std::numeric_limits::max(); // Prevent Start up spam UpdateFlag updateFlag = UpdateFlag::NEVER_UPDATED; } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 7472c243766..808a214b8cd 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -152,7 +152,8 @@ void TlmPacketizerTester ::sendPacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // No recently sent packet 1. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -164,7 +165,8 @@ void TlmPacketizerTester ::sendPacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // No recently sent packet 2. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(1, comBuff, static_cast(std::numeric_limits::max())); } void TlmPacketizerTester ::sendPacketLevelsTest() { @@ -220,7 +222,8 @@ void TlmPacketizerTester ::sendPacketLevelsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // No recently sent packet 1. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -232,7 +235,8 @@ void TlmPacketizerTester ::sendPacketLevelsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // No recently sent packet 2. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(1, comBuff, static_cast(std::numeric_limits::max())); } void TlmPacketizerTester ::updatePacketsTest() { @@ -270,7 +274,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // No recently sent packet 1. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -282,7 +287,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // No recently sent packet 1. Context set to sent counter, so this will be at max value. + ASSERT_from_PktSend(1, comBuff, static_cast(std::numeric_limits::max())); // second channel @@ -309,7 +315,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 1 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -331,7 +338,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 1 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -353,7 +361,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 3 + ASSERT_from_PktSend(0, comBuff, static_cast(3)); buff.resetSer(); ts.add(1, 0); @@ -377,7 +386,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -399,7 +409,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); //** Update all the packets again with new values @@ -423,7 +434,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 1 sent recently with a delta sched ticks of 4 + ASSERT_from_PktSend(0, comBuff, static_cast(4)); comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -435,7 +447,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(1, comBuff, static_cast(1)); // second channel @@ -458,7 +471,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(550))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 1 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -479,7 +493,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(550))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(211))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 1 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -501,7 +516,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 3 + ASSERT_from_PktSend(0, comBuff, static_cast(3)); buff.resetSer(); ts.add(1, 0); @@ -523,7 +539,8 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8649))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Packet 2 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); buff.resetSer(); ts.add(1, 0); @@ -544,8 +561,9 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(34441))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8649))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(65))); - - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + // Packet 2 sent recently with a delta sched ticks of 1 + ASSERT_from_PktSend(0, comBuff, static_cast(1)); } void TlmPacketizerTester ::ignoreTest() { @@ -583,7 +601,8 @@ void TlmPacketizerTester ::ignoreTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // First Packet 1 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -595,7 +614,8 @@ void TlmPacketizerTester ::ignoreTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(0))); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // First Packet 2 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(1, comBuff, static_cast(std::numeric_limits::max())); // ignored channel @@ -667,7 +687,8 @@ void TlmPacketizerTester ::sendManualPacketTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff1.serializeFrom(static_cast(15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff1.serializeFrom(static_cast(14))); - ASSERT_from_PktSend(0, comBuff1, static_cast(0)); + // First Packet 1 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(0, comBuff1, static_cast(std::numeric_limits::max())); Fw::ComBuffer comBuff2; ASSERT_EQ(Fw::FW_SERIALIZE_OK, @@ -679,7 +700,8 @@ void TlmPacketizerTester ::sendManualPacketTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff2.serializeFrom(static_cast(1010))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff2.serializeFrom(static_cast(15))); - ASSERT_from_PktSend(1, comBuff2, static_cast(0)); + // First Packet 2 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(1, comBuff2, static_cast(std::numeric_limits::max())); // should not be any new packets this->clearHistory(); @@ -700,7 +722,8 @@ void TlmPacketizerTester ::sendManualPacketTest() { this->invoke_to_Run(0, 0); this->component.doDispatch(); ASSERT_from_PktSend_SIZE(1); - ASSERT_from_PktSend(0, comBuff1, static_cast(0)); + // Packet 1 Sent recently. Delta Sched Ticks = 2 + ASSERT_from_PktSend(0, comBuff1, static_cast(2)); // another packet this->clearHistory(); @@ -722,7 +745,8 @@ void TlmPacketizerTester ::sendManualPacketTest() { this->invoke_to_Run(0, 0); this->component.doDispatch(); ASSERT_from_PktSend_SIZE(1); - ASSERT_from_PktSend(0, comBuff2, static_cast(0)); + // Packet 2 Sent recently. Delta Sched Ticks = 4 + ASSERT_from_PktSend(0, comBuff2, static_cast(4)); // Try to send invalid packet // send command to manually send a packet @@ -839,37 +863,10 @@ void TlmPacketizerTester ::setPacketLevelTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff1.serializeFrom(static_cast(0x15))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff1.serializeFrom(static_cast(0x14))); - ASSERT_from_PktSend(0, comBuff1, static_cast(0)); + // First Packet 1 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(0, comBuff1, static_cast(std::numeric_limits::max())); return; - - ASSERT_FROM_PORT_HISTORY_SIZE(2); - ASSERT_from_PktSend_SIZE(2); - - // construct the packet buffers and make sure they are correct - - Fw::ComBuffer comBuff; - ASSERT_EQ(Fw::FW_SERIALIZE_OK, - comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(14))); - - ASSERT_from_PktSend(0, comBuff, static_cast(0)); - - comBuff.resetSer(); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, - comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(20))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1000000))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1010))); - ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(15))); - - ASSERT_from_PktSend(1, comBuff, static_cast(0)); } void TlmPacketizerTester ::nonPacketizedChannelTest() { @@ -1107,8 +1104,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); - ASSERT_from_PktSend(1, comBuff, static_cast(0)); + // First Packet 1 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); + ASSERT_from_PktSend(1, comBuff, static_cast(std::numeric_limits::max())); // Pkt 2 comBuff.resetSer(); @@ -1121,7 +1119,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_from_PktSend(2, comBuff, static_cast(0)); + // First Packet 2 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(2, comBuff, static_cast(std::numeric_limits::max())); // Pkt 4 comBuff.resetSer(); @@ -1132,8 +1131,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); - ASSERT_from_PktSend(3, comBuff, static_cast(0)); - ASSERT_from_PktSend(4, comBuff, static_cast(0)); + // First Packet 4 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(3, comBuff, static_cast(std::numeric_limits::max())); + ASSERT_from_PktSend(4, comBuff, static_cast(std::numeric_limits::max())); this->clearHistory(); @@ -1174,7 +1174,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(12))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(11))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + + // First Packet 3 Send. Delta Sched Ticks = Max + ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); this->clearHistory(); @@ -1203,8 +1205,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - // Pkt 1 on Port 1 - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 1 on section 1 + // Pkt 1 on section 1 sent recently with a delta of 2 + ASSERT_from_PktSend(0, comBuff, static_cast(2)); this->clearHistory(); @@ -1223,9 +1226,10 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(this->m_portOutInvokes[0][3], 1); ASSERT_EQ(this->m_portOutInvokes[1][2], 0); - // Pkt 1 on Port 0 + // Pkt 1 on section 0 // comBuff unchanged since this->m_testTime is the same - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 1 on section 0 sent recently with a delta of 3 + ASSERT_from_PktSend(0, comBuff, static_cast(3)); this->clearHistory(); @@ -1255,7 +1259,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 2 on section 0 sent recently with a delta of 4 + ASSERT_from_PktSend(0, comBuff, static_cast(4)); this->clearHistory(); @@ -1282,7 +1287,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(this->m_portOutInvokes[0][3], 2); ASSERT_EQ(this->m_portOutInvokes[1][2], 0); - // Pkt 4 on Port 1 (Unchanged since T = 0) + // Pkt 4 on section 1 (Unchanged since T = 0) comBuff.resetSer(); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(Fw::ComPacketType::FW_PACKET_PACKETIZED_TLM))); @@ -1291,7 +1296,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(1))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 4 on section 0 sent recently with a delta of 6 + ASSERT_from_PktSend(0, comBuff, static_cast(6)); this->clearHistory(); @@ -1310,8 +1316,9 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(this->m_portOutInvokes[0][3], 2); ASSERT_EQ(this->m_portOutInvokes[1][2], 0); - // Pkt 4 on Port 0 (Unchanged since T = 0) - ASSERT_from_PktSend(0, comBuff, static_cast(0)); + // Pkt 4 on section 1 (Unchanged since T = 0) + // Pkt 4 on section 1 sent recently with a delta of 7 + ASSERT_from_PktSend(0, comBuff, static_cast(7)); this->clearHistory(); @@ -1363,8 +1370,10 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(22))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); - ASSERT_from_PktSend(0, comBuff, static_cast(0)); // Port 0 - ASSERT_from_PktSend(1, comBuff, static_cast(0)); // Port 1 + // Pkt 1 on section 0 sent recently with a delta of 9 + // Pkt 1 on section 1 sent recently with a delta of 10 + ASSERT_from_PktSend(0, comBuff, static_cast(9)); // Section 0 + ASSERT_from_PktSend(1, comBuff, static_cast(10)); // Section 1 // Pkt 2 comBuff.resetSer(); @@ -1377,7 +1386,8 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(3))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(4))); - ASSERT_from_PktSend(2, comBuff, static_cast(0)); // Port 0 + // Pkt 2 on section 0 sent recently with a delta of 8 + ASSERT_from_PktSend(2, comBuff, static_cast(8)); // Section 0 // Pkt 4 comBuff.resetSer(); @@ -1388,8 +1398,10 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(111))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(2))); - ASSERT_from_PktSend(3, comBuff, static_cast(0)); // Port 0 - ASSERT_from_PktSend(4, comBuff, static_cast(0)); // Port 1 + // Pkt 4 on section 0 sent recently with a delta of 6 + // Pkt 4 on section 1 sent recently with a delta of 5 + ASSERT_from_PktSend(3, comBuff, static_cast(6)); // Section 0 + ASSERT_from_PktSend(4, comBuff, static_cast(5)); // Section 1 } //! Configure telemetry enable logic diff --git a/default/config/TlmPacketizerCfg.fpp b/default/config/TlmPacketizerCfg.fpp index 7680d67447b..73a890ba8f2 100644 --- a/default/config/TlmPacketizerCfg.fpp +++ b/default/config/TlmPacketizerCfg.fpp @@ -6,6 +6,8 @@ module Svc { @ The number of sections of ports (Primary = 0, Secondary = 1, etc...) constant NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS = 3; - @ Greatest packet group (inclusive) + @ Greatest packet group constant MAX_CONFIGURABLE_TLMPACKETIZER_GROUP = 3; + + constant NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS = MAX_CONFIGURABLE_TLMPACKETIZER_GROUP + 1; } From 75c224ab6cf27eb593040f1fdfe64a14c678aa37 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Fri, 30 Jan 2026 15:27:53 -0600 Subject: [PATCH 15/16] Updated Telemetry --- Ref/Top/RefPackets.fppi | 3 +- Svc/TlmPacketizer/TlmPacketizer.cpp | 85 ++++++++++--------- Svc/TlmPacketizer/TlmPacketizer.fpp | 41 +++++---- Svc/TlmPacketizer/TlmPacketizer.hpp | 10 +-- .../test/ut/TlmPacketizerTester.cpp | 3 +- 5 files changed, 73 insertions(+), 69 deletions(-) diff --git a/Ref/Top/RefPackets.fppi b/Ref/Top/RefPackets.fppi index 1360f0490b0..1c1c850c5a5 100644 --- a/Ref/Top/RefPackets.fppi +++ b/Ref/Top/RefPackets.fppi @@ -18,7 +18,8 @@ telemetry packets RefPackets { ComCcsds.commsBufferManager.TotalBuffs ComCcsds.commsBufferManager.CurrBuffs ComCcsds.commsBufferManager.HiBuffs - #ComCcsds.tlmSend.SendLevel + #ComCcsds.tlmSend.GroupConfigs + #ComCcsds.tlmSend.SectionEnabled Ref.rateGroup1Comp.RgMaxTime Ref.rateGroup2Comp.RgMaxTime diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index eca7a4aa68c..357c40686af 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -115,30 +115,32 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, } // end packet list FW_ASSERT(maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, static_cast(maxLevel)); - // enable / disable appropriate groups - for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { - Fw::Enabled groupEnabled = group <= startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + + // Enable and set group configurations + for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { TlmPacketizer_RateLogic startRateLogic; - - for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { - this->m_groupConfigs[section][group].enabled = groupEnabled; - - switch (PACKET_UPDATE_MODE) { - case PACKET_UPDATE_ON_CHANGE: - startRateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; - break; - case PACKET_UPDATE_ALWAYS: - this->m_packetFlags[section][group].updateFlag = UpdateFlag::PAST; - startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; - break; - case PACKET_UPDATE_AFTER_FIRST_CHANGE: - startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; - break; - default: - FW_ASSERT(0, PACKET_UPDATE_MODE); - break; - } - this->m_groupConfigs[section][group].rateLogic = startRateLogic; + switch (PACKET_UPDATE_MODE) { + case PACKET_UPDATE_ON_CHANGE: + startRateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; + break; + case PACKET_UPDATE_ALWAYS: + for (PktSendCounters& pkt : this->m_packetFlags[section]) { + // Trigger sending packets even if they're empty. + pkt.updateFlag = UpdateFlag::PAST; + } + startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; + break; + case PACKET_UPDATE_AFTER_FIRST_CHANGE: + startRateLogic = TlmPacketizer_RateLogic::EVERY_MAX; + break; + default: + FW_ASSERT(0, PACKET_UPDATE_MODE); + break; + } + for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { + Fw::Enabled groupEnabled = group <= startLevel ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].set_enabled(groupEnabled); + this->m_groupConfigs[section][group].set_rateLogic(startRateLogic); } } @@ -385,7 +387,7 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { const FwIndexType outIndex = static_cast(section * NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS + static_cast(entryGroup)); PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; - GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; + TlmPacketizer_GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; /* Base conditions for sending 1. Output port is connected @@ -400,9 +402,9 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { if (pktEntryFlags.updateFlag == UpdateFlag::REQUESTED) { sendOutFlag = true; } else { - if (not((entryGroupConfig.enabled and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - entryGroupConfig.forceEnabled == Fw::Enabled::ENABLED)) continue; - if (entryGroupConfig.rateLogic == Svc::TlmPacketizer_RateLogic::SILENCED) continue; + if (not((entryGroupConfig.get_enabled() and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or + entryGroupConfig.get_forceEnabled() == Fw::Enabled::ENABLED)) continue; + if (entryGroupConfig.get_rateLogic() == Svc::TlmPacketizer_RateLogic::SILENCED) continue; if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data } @@ -417,8 +419,8 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { 3. Packet sent counter at MIN */ if (pktEntryFlags.updateFlag == UpdateFlag::NEW and - entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::EVERY_MAX and - pktEntryFlags.prevSentCounter >= entryGroupConfig.min) { + entryGroupConfig.get_rateLogic() != Svc::TlmPacketizer_RateLogic::EVERY_MAX and + pktEntryFlags.prevSentCounter >= entryGroupConfig.get_min()) { sendOutFlag = true; } @@ -426,8 +428,8 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { 1. Group Logic includes checking MAX 2. Packet set counter is at MAX */ - if (entryGroupConfig.rateLogic != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and - pktEntryFlags.prevSentCounter >= entryGroupConfig.max) { + if (entryGroupConfig.get_rateLogic() != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and + pktEntryFlags.prevSentCounter >= entryGroupConfig.get_max()) { sendOutFlag = true; } @@ -476,11 +478,10 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c } for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { - this->m_groupConfigs[section][group].enabled = - group <= level ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED; + this->m_groupConfigs[section][group].set_enabled(group <= level ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED); } } - this->tlmWrite_SendLevel(level); + this->tlmWrite_GroupConfigs(this->m_groupConfigs); this->log_ACTIVITY_HI_LevelSet(level); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -520,6 +521,7 @@ void TlmPacketizer ::ENABLE_SECTION_cmdHandler(FwOpcodeType opCode, return; } this->m_sectionEnabled[section] = enable; + this->tlmWrite_SectionEnabled(this->m_sectionEnabled); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -533,7 +535,8 @@ void TlmPacketizer ::ENABLE_GROUP_cmdHandler(FwOpcodeType opCode, this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } - this->m_groupConfigs[section][tlmGroup].enabled = enable; + this->m_groupConfigs[section][tlmGroup].set_enabled(enable); + this->tlmWrite_GroupConfigs(this->m_groupConfigs); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -546,7 +549,8 @@ void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } - this->m_groupConfigs[section][tlmGroup].forceEnabled = enable; + this->m_groupConfigs[section][tlmGroup].set_forceEnabled(enable); + this->tlmWrite_GroupConfigs(this->m_groupConfigs); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -561,10 +565,11 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } - GroupConfig& groupConfig = this->m_groupConfigs[section][tlmGroup]; - groupConfig.rateLogic = rateLogic; - groupConfig.min = minDelta; - groupConfig.max = maxDelta; + TlmPacketizer_GroupConfig& groupConfig = this->m_groupConfigs[section][tlmGroup]; + groupConfig.set_rateLogic(rateLogic); + groupConfig.set_min(minDelta); + groupConfig.set_max(maxDelta); + this->tlmWrite_GroupConfigs(this->m_groupConfigs); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } diff --git a/Svc/TlmPacketizer/TlmPacketizer.fpp b/Svc/TlmPacketizer/TlmPacketizer.fpp index 636ca17bd75..aeccd5a3f40 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.fpp +++ b/Svc/TlmPacketizer/TlmPacketizer.fpp @@ -5,6 +5,10 @@ module Svc { ) @ A component for storing telemetry active component TlmPacketizer { + # ---------------------------------------------------------------------- + # Types + # ---------------------------------------------------------------------- + enum RateLogic { SILENCED, EVERY_MAX, @@ -12,22 +16,23 @@ module Svc { ON_CHANGE_MIN_OR_EVERY_MAX, } - # struct GroupConfig { - # enabled: Fw.Enabled, - # forceEnabled: Fw.Enabled, - # rateLogic: RateLogic, - # min: U32, - # max: U32 - # } default { - # enabled = Fw.Enabled.ENABLED, - # forceEnabled = Fw.Enabled.DISABLED, - # rateLogic = RateLogic.ON_CHANGE_MIN, - # min = 0, - # max = 0, - # } - - # array GroupConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] GroupConfig - # array SectionConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS] GroupConfigs + struct GroupConfig { + enabled: Fw.Enabled @< Enable / Disable Telemetry Output + forceEnabled: Fw.Enabled @< Force Enable / Disable Telemetry Output + rateLogic: RateLogic @< Rate Logic Configuration + min: U32 @< Minimum Sched Ticks + max: U32 @< Maximum Sched Ticks + } default { + enabled = Fw.Enabled.ENABLED + forceEnabled = Fw.Enabled.DISABLED + rateLogic = RateLogic.ON_CHANGE_MIN + min = 0 + max = 0 + } + + array GroupConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS] GroupConfig + array SectionConfigs = [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS] GroupConfigs + array SectionEnabled = [NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS] Fw.Enabled default Fw.Enabled.ENABLED # ---------------------------------------------------------------------- # General ports @@ -188,8 +193,8 @@ module Svc { # ---------------------------------------------------------------------- @ Telemetry send level - telemetry SendLevel: FwChanIdType id 0 - # telemetry GroupConfigs: SectionConfigs id 0 + telemetry GroupConfigs: SectionConfigs id 0 + telemetry SectionEnabled: SectionEnabled id 1 } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 221eccd382d..2830d8bb10d 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -182,15 +182,9 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { TlmEntry* findBucket(FwChanIdType id); - Fw::Enabled m_sectionEnabled[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS]{}; + TlmPacketizer_SectionEnabled m_sectionEnabled{}; - struct GroupConfig { - Fw::Enabled enabled = Fw::Enabled::ENABLED; - Fw::Enabled forceEnabled = Fw::Enabled::DISABLED; - TlmPacketizer_RateLogic rateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; - U32 min = 0; //!< Default for Backwards Compatible Behavior - U32 max = 0; //!< Default for Backwards Compatible Behavior - } m_groupConfigs[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS]{}; + TlmPacketizer_SectionConfigs m_groupConfigs{}; enum UpdateFlag : U8 { NEVER_UPDATED = 0, diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index 808a214b8cd..f2bdaf6a612 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -810,8 +810,7 @@ void TlmPacketizerTester ::setPacketLevelTest() { ASSERT_EVENTS_LevelSet_SIZE(1); ASSERT_EVENTS_LevelSet(0, 1); ASSERT_TLM_SIZE(1); - ASSERT_TLM_SendLevel_SIZE(1); - ASSERT_TLM_SendLevel(0, 1); + ASSERT_TLM_GroupConfigs_SIZE(1); // send the packets // first channel From c6d75ce7511cad17bd86dcda08bb8487b3c99a00 Mon Sep 17 00:00:00 2001 From: Alex Mariano Date: Fri, 30 Jan 2026 15:32:11 -0600 Subject: [PATCH 16/16] Spelling and Format --- Svc/TlmPacketizer/TlmPacketizer.cpp | 58 ++++++++++--------- Svc/TlmPacketizer/TlmPacketizer.hpp | 6 +- Svc/TlmPacketizer/docs/sdd.md | 4 +- .../test/ut/TlmPacketizerTester.cpp | 8 +-- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Svc/TlmPacketizer/TlmPacketizer.cpp b/Svc/TlmPacketizer/TlmPacketizer.cpp index 357c40686af..f8b42c2d361 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.cpp +++ b/Svc/TlmPacketizer/TlmPacketizer.cpp @@ -115,14 +115,13 @@ void TlmPacketizer::setPacketList(const TlmPacketizerPacketList& packetList, } // end packet list FW_ASSERT(maxLevel <= MAX_CONFIGURABLE_TLMPACKETIZER_GROUP, static_cast(maxLevel)); - // Enable and set group configurations for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { TlmPacketizer_RateLogic startRateLogic; switch (PACKET_UPDATE_MODE) { case PACKET_UPDATE_ON_CHANGE: startRateLogic = TlmPacketizer_RateLogic::ON_CHANGE_MIN; - break; + break; case PACKET_UPDATE_ALWAYS: for (PktSendCounters& pkt : this->m_packetFlags[section]) { // Trigger sending packets even if they're empty. @@ -378,60 +377,61 @@ void TlmPacketizer ::Run_handler(const FwIndexType portNum, U32 context) { // Iterate through output sections for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { // Packet is updated and not REQUESTED (Prevent Downgrading) - if (this->m_sendBuffers[pkt].updated and this->m_packetFlags[section][pkt].updateFlag != UpdateFlag::REQUESTED) { + if (this->m_sendBuffers[pkt].updated and + this->m_packetFlags[section][pkt].updateFlag != UpdateFlag::REQUESTED) this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::NEW; - } bool sendOutFlag = false; const FwIndexType outIndex = static_cast(section * NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS + - static_cast(entryGroup)); + static_cast(entryGroup)); PktSendCounters& pktEntryFlags = this->m_packetFlags[section][pkt]; TlmPacketizer_GroupConfig& entryGroupConfig = this->m_groupConfigs[section][entryGroup]; - + /* Base conditions for sending 1. Output port is connected 2. The packet was requested (Override Checks). - + If the packet wasn't requested: 3. The Section and Group in Section is enabled OR the Group in Section is force enabled 4. The rate logic is not SILENCED. 5. The packet has data (marked updated in the past or new) */ - if (not this->isConnected_PktSend_OutputPort(outIndex)) continue; + if (not this->isConnected_PktSend_OutputPort(outIndex)) + continue; if (pktEntryFlags.updateFlag == UpdateFlag::REQUESTED) { sendOutFlag = true; } else { if (not((entryGroupConfig.get_enabled() and this->m_sectionEnabled[section] == Fw::Enabled::ENABLED) or - entryGroupConfig.get_forceEnabled() == Fw::Enabled::ENABLED)) continue; - if (entryGroupConfig.get_rateLogic() == Svc::TlmPacketizer_RateLogic::SILENCED) continue; - if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) continue; // Avoid No Data + entryGroupConfig.get_forceEnabled() == Fw::Enabled::ENABLED)) + continue; + if (entryGroupConfig.get_rateLogic() == Svc::TlmPacketizer_RateLogic::SILENCED) + continue; + if (pktEntryFlags.updateFlag == UpdateFlag::NEVER_UPDATED) + continue; // Avoid No Data } - - // Update Counter, prevent overflow. - if (pktEntryFlags.prevSentCounter < std::numeric_limits::max()) { + + // Update Counter, prevent overflow. + if (pktEntryFlags.prevSentCounter < std::numeric_limits::max()) pktEntryFlags.prevSentCounter++; - } - + /* 1. Packet has been updated 2. Group Logic includes checking MIN 3. Packet sent counter at MIN */ - if (pktEntryFlags.updateFlag == UpdateFlag::NEW and + if (pktEntryFlags.updateFlag == UpdateFlag::NEW and entryGroupConfig.get_rateLogic() != Svc::TlmPacketizer_RateLogic::EVERY_MAX and - pktEntryFlags.prevSentCounter >= entryGroupConfig.get_min()) { + pktEntryFlags.prevSentCounter >= entryGroupConfig.get_min()) sendOutFlag = true; - } /* 1. Group Logic includes checking MAX 2. Packet set counter is at MAX */ if (entryGroupConfig.get_rateLogic() != Svc::TlmPacketizer_RateLogic::ON_CHANGE_MIN and - pktEntryFlags.prevSentCounter >= entryGroupConfig.get_max()) { + pktEntryFlags.prevSentCounter >= entryGroupConfig.get_max()) sendOutFlag = true; - } // Send under the following conditions: // 1. Packet received updates and it has been past delta min counts since last packet (min enabled) @@ -478,7 +478,8 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c } for (FwIndexType section = 0; section < NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS; section++) { for (FwChanIdType group = 0; group < NUM_CONFIGURABLE_TLMPACKETIZER_GROUPS; group++) { - this->m_groupConfigs[section][group].set_enabled(group <= level ? Fw::Enabled::ENABLED : Fw::Enabled::DISABLED); + this->m_groupConfigs[section][group].set_enabled(group <= level ? Fw::Enabled::ENABLED + : Fw::Enabled::DISABLED); } } this->tlmWrite_GroupConfigs(this->m_groupConfigs); @@ -486,7 +487,10 @@ void TlmPacketizer ::SET_LEVEL_cmdHandler(const FwOpcodeType opCode, const U32 c this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cmdSeq, const U32 id, const FwIndexType section) { +void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, + const U32 cmdSeq, + const U32 id, + const FwIndexType section) { FwChanIdType pkt = 0; for (pkt = 0; pkt < this->m_numPackets; pkt++) { if (this->m_fillBuffers[pkt].id == id) { @@ -494,7 +498,7 @@ void TlmPacketizer ::SEND_PKT_cmdHandler(const FwOpcodeType opCode, const U32 cm this->m_fillBuffers[pkt].updated = true; this->m_fillBuffers[pkt].latestTime = this->getTime(); this->m_lock.unLock(); - + this->m_packetFlags[section][pkt].updateFlag = UpdateFlag::REQUESTED; this->log_ACTIVITY_LO_PacketSent(id); @@ -545,7 +549,8 @@ void TlmPacketizer ::FORCE_GROUP_cmdHandler(FwOpcodeType opCode, FwIndexType section, FwChanIdType tlmGroup, Fw::Enabled enable) { - if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or + tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } @@ -561,7 +566,8 @@ void TlmPacketizer ::SET_GROUP_DELTAS_cmdHandler(FwOpcodeType opCode, Svc::TlmPacketizer_RateLogic rateLogic, U32 minDelta, U32 maxDelta) { - if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { + if (section < 0 or section >= NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS or + tlmGroup > MAX_CONFIGURABLE_TLMPACKETIZER_GROUP) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); return; } diff --git a/Svc/TlmPacketizer/TlmPacketizer.hpp b/Svc/TlmPacketizer/TlmPacketizer.hpp index 2830d8bb10d..dafddbcab7b 100644 --- a/Svc/TlmPacketizer/TlmPacketizer.hpp +++ b/Svc/TlmPacketizer/TlmPacketizer.hpp @@ -169,7 +169,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { // hash function for looking up telemetry channel FwChanIdType doHash(FwChanIdType id); - Os::Mutex m_lock; //!< used to lock access to packet buffers + Os::Mutex m_lock; //!< used to lock access to packet buffers bool m_configured; //!< indicates a table has been passed and packets configured @@ -183,7 +183,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { TlmEntry* findBucket(FwChanIdType id); TlmPacketizer_SectionEnabled m_sectionEnabled{}; - + TlmPacketizer_SectionConfigs m_groupConfigs{}; enum UpdateFlag : U8 { @@ -194,7 +194,7 @@ class TlmPacketizer final : public TlmPacketizerComponentBase { }; struct PktSendCounters { - U32 prevSentCounter = std::numeric_limits::max(); // Prevent Start up spam + U32 prevSentCounter = std::numeric_limits::max(); // Prevent Start up spam UpdateFlag updateFlag = UpdateFlag::NEVER_UPDATED; } m_packetFlags[NUM_CONFIGURABLE_TLMPACKETIZER_SECTIONS][MAX_PACKETIZER_PACKETS]{}; }; diff --git a/Svc/TlmPacketizer/docs/sdd.md b/Svc/TlmPacketizer/docs/sdd.md index 0d2f5a8c518..23d48ac2880 100644 --- a/Svc/TlmPacketizer/docs/sdd.md +++ b/Svc/TlmPacketizer/docs/sdd.md @@ -4,7 +4,7 @@ The `Svc::TlmPacketizer` Component is used to store telemetry values written by other components. The values are stored in serialized form. TlmPacketizer differs from `Svc::TlmChan` in that it stores telemetry in defined packets instead of streaming the updates as they come. The defined packets are passed in as a table to the `setPacketList()` public method. When telemetry updates are passed to the component, they are placed at the offset in a packet buffer defined by the table. When the `run()` port is called, all the defined packets are sent to the output port with the most recent . This is meant to replace `Svc::TlmCham` for use cases where a more compact packet format is desired. The disadvantage is that all channels are pushed whether or not they have been updated. -Uses can change the individual rates at which groups per group instance are outputted upon a `run()` sched tick. Each group on each output section has independantly configurable telemetry resampling rates. Packets can be sent on change with a rate limiting enforced minimum number of ticks between updates. Or packets can be sent with at a guaranteed rate of a maximum number of ticks between updates. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. +Uses can change the individual rates at which groups per group instance are outputted upon a `run()` sched tick. Each group on each output section has independently configurable telemetry resampling rates. Packets can be sent on change with a rate limiting enforced minimum number of ticks between updates. Or packets can be sent with at a guaranteed rate of a maximum number of ticks between updates. Packets are evaluated individually and have a counter since last invocation. MIN and MAX logic are selectable, for users that desire only "on change" telemetry, MAX invocations between telemetry points, both, or none at all. ## 2. Requirements @@ -67,7 +67,7 @@ The `Svc::TlmPacketizer` is configurable to have multiple `PktSend` group output `PktSend` output is ordered by `[section][group]`, where within each section telemetry groups are emitted. Each group within each section are separately sampled, and have their own configuration rates and logic independent from each other. -Each group is configured with min/max delta parameters, as well as a logic gate determining its output rate behavior. Deltas are counters of sched ticks invoked through the `run()` port. Min Delta is the least number of `Run()` invocations between successive packets before a packet with an updated channel is allowed to be sent. Updated packets will not be sent until their counter reaches Min Delta. This mitigates against telemetry spam while allowing users to benefit from asyncrhonously updating channels (e.g. those that don't occupy a set schedule). Max Delta is the maximum number of `Run()` invocations between successive packets. Upon reaching this counter, the packet would be sent, regardless of change. +Each group is configured with min/max delta parameters, as well as a logic gate determining its output rate behavior. Deltas are counters of sched ticks invoked through the `run()` port. Min Delta is the least number of `Run()` invocations between successive packets before a packet with an updated channel is allowed to be sent. Updated packets will not be sent until their counter reaches Min Delta. This mitigates against telemetry spam while allowing users to benefit from asynchronously updating channels (e.g. those that don't occupy a set schedule). Max Delta is the maximum number of `Run()` invocations between successive packets. Upon reaching this counter, the packet would be sent, regardless of change. Each telemetry group per section is configured with a `RateLogic` parameter: * `SILENCED`: Packet will never be sent diff --git a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp index f2bdaf6a612..2a85d7c28f7 100644 --- a/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp +++ b/Svc/TlmPacketizer/test/ut/TlmPacketizerTester.cpp @@ -561,7 +561,7 @@ void TlmPacketizerTester ::updatePacketsTest() { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(34441))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(8649))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(65))); - + // Packet 2 sent recently with a delta sched ticks of 1 ASSERT_from_PktSend(0, comBuff, static_cast(1)); } @@ -1173,7 +1173,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(12))); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(this->m_testTime)); ASSERT_EQ(Fw::FW_SERIALIZE_OK, comBuff.serializeFrom(static_cast(11))); - + // First Packet 3 Send. Delta Sched Ticks = Max ASSERT_from_PktSend(0, comBuff, static_cast(std::numeric_limits::max())); @@ -1371,7 +1371,7 @@ void TlmPacketizerTester ::configuredTelemetryGroupsTests(void) { // Pkt 1 on section 0 sent recently with a delta of 9 // Pkt 1 on section 1 sent recently with a delta of 10 - ASSERT_from_PktSend(0, comBuff, static_cast(9)); // Section 0 + ASSERT_from_PktSend(0, comBuff, static_cast(9)); // Section 0 ASSERT_from_PktSend(1, comBuff, static_cast(10)); // Section 1 // Pkt 2 @@ -1424,7 +1424,7 @@ void TlmPacketizerTester ::advancedControlGroupTests(void) { ASSERT_FROM_PORT_HISTORY_SIZE(3); ASSERT_from_PktSend_SIZE(3); this->clearHistory(); - + // Send a packet every time the port is invoked. this->sendCmd_SET_GROUP_DELTAS(0, 0, 0, 1, Svc::TlmPacketizer_RateLogic::EVERY_MAX, 0, 0); this->component.doDispatch();