10 KiB
NetworkValue and NetworkComponent Enhancements
1. Overview
This document outlines a plan to enhance the NetworkComponent and NetworkValue classes to provide more granular, runtime control over individual NetworkValue instances and their specific features (Modbus, Protobuf, Logging).
2. Motivation
Currently, the NetworkComponent provides a single m_enabled coil that toggles the entire component's loopNetwork() method on or off. While useful, this is an all-or-nothing approach.
There is a need for more fine-grained control, allowing a user to selectively enable or disable not just the NetworkValue itself, but also its specific features. This would be useful for:
- Debugging: Selectively enabling logging for a single problematic
NetworkValue. - Performance: Disabling high-frequency Protobuf or Modbus updates for values that are not currently being monitored by any client, without disabling the value's core logic.
- Bandwidth Management: Remotely toggling which values are broadcast over the network to reduce traffic.
3. Proposed Feature: Feature Control Masks
I propose adding a series of new, default Modbus registers to the NetworkComponent.
- Location: These registers will start at
mb_tcp_base_address() + 1. - Type: They will be
uint16_tholding registers. - Functionality: Each register will act as a bitmask for a specific feature. Each bit in a mask will correspond to a
NetworkValueinstance within the_networkValuesvector.
a. New Registers
| Offset | Name | Description | Default |
|---|---|---|---|
| +0 | ENABLED |
(Coil) Master switch for the component's loopNetwork(). |
1 (On) |
| +1 | ENABLED_MASK |
(Holding) Bitmask to enable/disable entire NetworkValue instances. |
0xFFFF |
| +2 | MODBUS_MASK |
(Holding) Bitmask to enable/disable the Modbus feature for each NetworkValue. |
0xFFFF |
| +3 | PROTOBUF_MASK |
(Holding) Bitmask to enable/disable the Protobuf feature for each NetworkValue. |
0xFFFF |
| +4 | LOGGING_MASK |
(Holding) Bitmask to enable/disable the Logging feature for each NetworkValue. |
0xFFFF |
b. NetworkComponent Implementation
- New
NetworkValueMembers: NewNetworkValue<uint16_t>members will be added form_enabled_mask,m_modbus_mask,m_protobuf_mask, andm_logging_mask. - Initialization: In
setup(), these newNetworkValues will be initialized and registered as individual holding registers at their respective offsets. Their default value will be0xFFFF. - Update Logic: An
onUpdatecallback will be implemented for each mask. When a mask is written to, its callback will iterate through the_networkValuesvector and apply the corresponding feature toggle.
c. NetworkValue Implementation
The NetworkValue::update() method will be modified to respect the component's top-level enabled flag before proceeding with any change detection or notifications.
4. Implementation Steps
- Create Documentation: Write this plan. (Done)
- Modify
NetworkComponent.h:- Add the new
NetworkValue<uint16_t>members for each mask. - Update
setup()to register the new Modbus holding registers individually. - Implement the
onUpdatecallbacks for each mask.
- Add the new
- Modify
NetworkValue.h:- Add the
if (!this->enabled()) return false;guard at the beginning of theupdate()method.
- Add the
- Verify & Test: Run the build and update the
NetworkValueTestPBcomponent to verify the new functionality.
5. Edge Cases and Limitations
- Maximum NetworkValues: The use of a
uint16_tfor the bitmasks limits a singleNetworkComponentto a maximum of 16NetworkValueinstances. - Vector Order: The masks rely on the fixed order of
NetworkValueinstances in the_networkValuesvector. This order must not change after initialization. - Component vs. NetworkValue State: Bit 0 of the
ENABLED_MASKcontrols the entire component'sloopNetwork()method. Other bits inENABLED_MASKcontrol individualNetworkValueinstances. The feature-specific masks only control their respective features and do not stop theNetworkValuefrom updating its internal state.
6. Performance Considerations (ESP32)
Based on the provided device metrics (Free Heap: ~46 KB, Max Free Block: ~34 KB), memory is a significant constraint. Let's analyze the impact of this new feature.
a. Memory (RAM) Impact
- Per
NetworkComponent:- Each new feature mask adds one
NetworkValue<uint16_t>instance. ANetworkValueobject has a baseline size of around 40-50 bytes. - Adding 4 new masks per
NetworkComponentwill consume approximately 160-200 bytes of RAM.
- Each new feature mask adds one
- System-Wide:
- With 30 components, the total RAM increase would be
30 * 200 bytes = 6000 bytes, or approximately 5.9 KB. - This represents a ~13% increase relative to the 46 KB of free heap.
- With 30 components, the total RAM increase would be
Detailed Memory Breakdown
The total memory footprint is a sum of the NetworkValue objects themselves and the storage for their MB_Registers structs within the parent NetworkComponent.
| Item | Size per Item (Bytes) | Notes |
|---|---|---|
MB_Registers struct |
~32 | Includes ushort fields, pointers, and compiler padding. |
NetworkValue overhead |
~24 | Includes v-table pointer, owner, flags, callbacks, etc. |
Total per NetworkValue |
~56 |
With this baseline, we can calculate a more precise per-component and system-wide impact. This example assumes a component with 10 user-defined NetworkValues, in addition to the new feature masks.
| Item | Count (per Component) | Total Size (Bytes) |
|---|---|---|
NetworkValue Objects |
||
m_enabled_mask |
1 | 56 |
m_modbus_mask |
1 | 56 |
m_protobuf_mask |
1 | 56 |
m_logging_mask |
1 | 56 |
_modbusBlocks Array |
||
For user NetworkValues |
10 | 320 |
For mask NetworkValues |
4 | 128 |
| Total Per Component | ~672 | |
| Total System-Wide | 30 Components | ~20,160 (~19.7 KB) |
- A ~20 KB increase represents a ~43% reduction in the available 46 KB of free heap. This is a very significant impact and must be carefully considered.
b. CPU Impact
- Idle State: When no masks are being changed, the CPU impact is negligible. It amounts to a few extra
ifchecks in themb_tcp_writehandler per incoming write request. - On Mask Update: When a mask register is written to, the
onUpdatecallback will execute. This involves a loop of up to 16 iterations (the number ofNetworkValueinstances). Inside the loop, it performs a bitwise check and callsenable()orenableFeature(). This is a very fast operation, likely taking only a few microseconds to complete.
c. Conclusion
The primary impact of this feature is on RAM. A ~16 KB increase is a major architectural cost. While the feature offers significant flexibility, the memory overhead may be too high for the target device. The "Consolidated Mask Register" optimization should be considered the primary implementation path to mitigate this.
7. Optimizations and Future Work
- Consolidated Mask Register: (HIGHLY RECOMMENDED) To significantly reduce the memory footprint, the four separate
uint16_tmasks should be consolidated into a singlestd::array<uint16_t, 4>. This would require only oneNetworkValueinstance and one Modbus block (FN_WRITE_MULT_REGISTERS) to update all masks at once, reducing the per-component RAM overhead from over 500 bytes to around 120 bytes. - Dynamic Instantiation: For very large or dynamic systems, the static
NetworkComponent<N>template could be replaced with a version that uses a dynamically allocatedVectorfor_modbusBlocks. This would increase memory management complexity but allow for a variable number ofNetworkValueinstances. - Read-Only Masks: To provide a read-only view of the current feature states, additional read-only registers could be exposed. These would be updated internally whenever a mask is changed, providing a clear "live" view of the configuration for clients without allowing them to be written to directly.
Radical Optimizations (Advanced)
For systems requiring extreme memory optimization, the NetworkComponent and NetworkValue architecture could be fundamentally redesigned.
-
Centralized
NetworkValueRegistry: Instead of eachNetworkComponentmanaging its own list ofNetworkValueinstances and Modbus blocks, a single, staticNetworkValueRegistrycould be implemented.- Memory Savings: This would eliminate the
std::vector _networkValuesandMB_Registers* _modbusBlocksfrom everyNetworkComponentinstance, saving hundreds of bytes per component. - Implementation: Components would register their
NetworkValues with this central registry during theirsetup()phase. TheModbusTCPmanager would then interact directly with the registry instead of individual components.
- Memory Savings: This would eliminate the
-
Flyweight Pattern for Modbus Data: The
MB_Registersstruct could be replaced with a more memory-efficient flyweight object.- How it Works: The central registry would store a single, canonical copy of the static Modbus metadata (name, group, function code, etc.) for each registered address. The
NetworkValuewould only need to store a pointer or an ID to this shared data, rather than a full copy. - Memory Savings: This would further reduce the per-
NetworkValueoverhead from ~56 bytes to closer to ~24-32 bytes.
- How it Works: The central registry would store a single, canonical copy of the static Modbus metadata (name, group, function code, etc.) for each registered address. The
-
Callback-Based Read/Write: The virtual
mb_tcp_readandmb_tcp_writemethods could be replaced with a callback system.- How it Works: When registering a
NetworkValue, the component would provide lambdas or function pointers for getting and setting its value. The central registry would store these pointers. When a Modbus request arrives, the registry would directly invoke the appropriate callback. - Performance: This would eliminate the overhead and indirection of virtual function calls through the component hierarchy, resulting in faster request handling.
- How it Works: When registering a
-
Combined Approach: A combination of all three radical optimizations would yield the most significant memory and performance gains, but would require a substantial refactoring of the current component architecture.