firmware-base/playground/docs/networked-component.md

3.4 KiB
Raw Blame History

Network-enabled Components Incremental Design Notes

The objective is to remove repetitive boiler-plate around Modbus exposure while staying 100 % compatible with the existing NetworkValue feature set and the current component hierarchy.

1. Helper mix-in: ModbusBindable<N>

  • Template parameter N = compile-time count of Modbus fields a component wants to expose.
  • Derives from Component (exactly like ModbusDevice<N> today) and pre-allocates the MB_Registers array plus view.
  • Adds two convenience wrappers:
    1. registerField(id, name, fnCode, access) returns reference to the freshly allocated MB_Registers entry so the caller can immediately call .bind(myNetworkValue).
    2. bind(NetworkValue<T>& nv) copies the addressing information and keeps a pointer so that later enable() / disable() can toggle NetworkValueFeatureFlags::MODBUS for the field in one place.

This reduces a typical component constructor to a short chain:

using NVf = NetworkValue<int>;
_modbus.registerField(Temperature, "Temp", FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY)
       .bind(_temperatureNV);

No manual index math, no local _modbusBlocks[i] = ….

2. Runtime toggle

Expose two helpers:

void Component::enableNet()   { SBI(nFlags, OBJECT_NET_CAPS::E_NCAPS_MODBUS); }
void Component::disableNet()  { CBI(nFlags, OBJECT_NET_CAPS::E_NCAPS_MODBUS); }

ModbusBindable observes the flag in its loop() and calls .enableFeature(MODBUS) or .disableFeature(MODBUS) on every bound NetworkValue once when the state flips.

3. Stable logical identifiers

Today the only handle for a field is its offset / absolute address and a free-form name. To decouple user-code from register maps introduce:

  1. A small enum class per component describing its logical fields e.g. enum class OmronField : uint8_t { PV, SP, StatusLow, … }.
  2. Each MB_Registers gains a new member uint8_t logicalId holding the cast value of that enum.
  3. Helpers byId(OmronField f) and mb_tcp_write(logicalId, value) eliminate hard-coded offsets in the business logic.

Address computation stays automatic inside the registerField() helper which receives the enum value and returns the concrete startAddress.

4. Optional syntactic sugar field builder

A single-line macro speeds up definition:

#define NV_FIELD(enumId, nvRef, fn, acc) \
    registerField(enumId, #enumId, fn, acc).bind(nvRef)

Example:

NV_FIELD(OmronField::PV,   _pv,  FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY);
NV_FIELD(OmronField::SP,   _sp,  FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE);

5. Migration path

  1. Keep ModbusDevice<N> working untouched.
  2. Introduce ModbusBindable<N> side-by-side early adopters can switch gradually.
  3. After all components migrate, retire the older mix-in.

6. Minimal runtime footprint

All helpers are constexpr / inline; no dynamic allocation, no RTTI, no std containers fits ESP32 & PlatformIO constraints.

7. Quick architecture overview

graph TD
  Comp("Component") -->|mixin| MB["ModbusBindable<N>"]
  MB --> NV("NetworkValue<T>")
  MB --> Regs["MB_Registers[N]"]
  Comp -- enable/disable --> MB

Next step: draft the actual ModbusBindable header and refactor one small component (NetworkValueTest) as proof-of-concept.