Value Encoding (DPT Helper)
Header-only, opt-in, zero-cost helpers that convert application values to and from KNX Datapoint Type wire bytes, fully portable across every target.
Overview
The DPT helpers convert your application values (°C, %, counters, scenes, dates…) into the KNX Datapoint Type (DPT) wire format and read them back. They are the bridge between raw group-object bytes and typed C++ values.
They are fully portable — pure value math with no hardware, no operating system, and no platform assumptions. Everything is:
- Header-only. All logic lives in the
dist/include/Stack/Helpers/Dpt/*.hheaders; there is no library code to link. - Opt-in and zero-cost. A consumer that does not call a helper pays nothing:
with
--gc-sectionsevery uncalled function is dropped from flash and RAM. - Exception-free and heap-free. Nothing throws and nothing allocates.
- Deterministic. All math is
float(neverdouble) with nolibm, so a host (x86 SSE) build and a Cortex-M3 soft-float build produce bit-identical results.
The codecs are clean-room derived from KNX Vol 3 (03_07_02, plus AN188 / AN197).
Because the helpers are header-only, they compile into your application, not into
the prebuilt .a. They work identically on any target you port the stack to — see
Porting to a New Platform.
Include & namespace
#include <Stack/Helpers/DptHelper.h> // pulls in every DPT family// Include only the family you need for faster compiles:
#include <Stack/Helpers/Dpt/DptFloat.h> // DPT-9, DPT-14
#include <Stack/Helpers/Dpt/DptScalar.h> // DPT-1/2/3/5/6/7/8/12/13/17/18/20
#include <Stack/Helpers/Dpt/DptString.h> // DPT-16
#include <Stack/Helpers/Dpt/DptDateTime.h> // DPT-10/11/19Every type lives in namespace Dpt. Names carry the DPT code: Dpt::D<main> is the
family codec, and Dpt::D<main>_<sub> is the semantic sub-type (it documents the range
and unit). For example Dpt::D9 is the 2-byte float family and Dpt::D9_001 is
temperature in °C.
The uniform contract
Every DPT type exposes the same shape:
| Member | Signature | What it does |
|---|---|---|
kSize | static constexpr uint8_t | Wire size in bytes. Use it to size a buffer. |
Encode | uint8_t Encode(T value, uint8_t* out) | Writes kSize bytes into out, returns the number of bytes written. |
Decode | T Decode(ByteSpan in) | Primary, value-returning form. On malformed input returns a defined default (0 / false). |
TryDecode | bool TryDecode(ByteSpan in, T& out) | Validating form: wrong length / reserved bit / out-of-range → false. |
EncodeByte | uint8_t EncodeByte(...) | For ≤1-byte types: returns a single uint8_t to feed SetValueAsByte directly. |
EncodeWord | uint16_t EncodeWord(...) | For ≤2-byte types: returns a single uint16_t to feed SetValueAsFloat directly. DPT-9 also offers DecodeWord. |
ByteSpan is the existing public view type (<Stack/Helpers/ByteSpan.h>):
ByteSpan(buf, len) wraps a buffer and gives you .data() and .size().
Decode vs TryDecode
Which one? Use Decode when you will use the value directly (display, log): it
returns 0.0f on bad input and never crashes — float t = Dpt::D9_001::Decode(span);.
Use TryDecode when the input is untrusted or validity matters: if (Dpt::D9_001::TryDecode(span, t)) { … }.
Encode is lenient (it masks fields), TryDecode is strict (it checks range and
reserved bits).
DPT families
Bridging to group objects
A DeviceObject holds the raw value; it does not carry a DPT number. The helper is the
bridge between your typed value and the group-object sinks:
// Byte object (DPT-5/6/17/18/20):
device.SetValueAsByte(Dpt::D5_001::EncodeByte(50.0f));
float pct = Dpt::D5_001::Decode(dev.GetValueSpan());
// 2-byte float object (DPT-9):
dev.SetValueAsFloat(Dpt::D9_001::EncodeWord(21.0f)); // -> 0x0C1A word
float t = Dpt::D9_001::Decode(dev.GetValueSpan());
// Multi-byte object (DPT-14/12/13/16/10/11/19):
uint8_t buf[Dpt::D19_001::kSize];
Dpt::D19_001::Encode(dt, buf);
dev.SetValue(ByteSpan(buf, Dpt::D19_001::kSize));Two complete examples:
// Publish a temperature sensor value to a 2-byte float group object:
void PublishTemperature(DeviceObject& tempGo, float celsius) {
tempGo.SetValueAsFloat(Dpt::D9_001::EncodeWord(celsius)); // 0x7FFF ("invalid") on NaN
}
// Read an incoming group object as %RH:
bool ReadHumidity(DeviceObject& rhGo, float& outPct) {
return Dpt::D9_007::TryDecode(rhGo.GetValueSpan(), outPct);
}See Group Objects for the SetValueAsBit /
SetValueAsByte / SetValueAsFloat / SetValue sink API and GetValueSpan().
Notes & guarantees
- Encode lenient, TryDecode strict.
Encodemasks fields (e.g.hour & 0x1F);TryDecodechecks range and reserved bits. When the input is untrusted, preferTryDecodeoverDecode. - Deterministic float. All math is
float(neverdouble) with nolibm, so a host (x86 SSE) build and a Cortex-M3 soft-float build return identical results. - DPT-9 goldens were derived from the AN188 spec, not captured from any codec.
- Calendar caveats. Codecs handle the wire encoding only. Century interpretation (DPT-11) and time-zone / flag semantics (DPT-19) are the caller's responsibility.
- Coverage. The ~15 family codecs cover all ETS encodings; named sub-types are added
in phases (see
DPT_HELPER_PHASE2_TODO.md).