Kbox-UniStack Docs

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/*.h headers; there is no library code to link.
  • Opt-in and zero-cost. A consumer that does not call a helper pays nothing: with --gc-sections every uncalled function is dropped from flash and RAM.
  • Exception-free and heap-free. Nothing throws and nothing allocates.
  • Deterministic. All math is float (never double) with no libm, 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/19

Every 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:

MemberSignatureWhat it does
kSizestatic constexpr uint8_tWire size in bytes. Use it to size a buffer.
Encodeuint8_t Encode(T value, uint8_t* out)Writes kSize bytes into out, returns the number of bytes written.
DecodeT Decode(ByteSpan in)Primary, value-returning form. On malformed input returns a defined default (0 / false).
TryDecodebool TryDecode(ByteSpan in, T& out)Validating form: wrong length / reserved bit / out-of-range → false.
EncodeByteuint8_t EncodeByte(...)For ≤1-byte types: returns a single uint8_t to feed SetValueAsByte directly.
EncodeWorduint16_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. Encode masks fields (e.g. hour & 0x1F); TryDecode checks range and reserved bits. When the input is untrusted, prefer TryDecode over Decode.
  • Deterministic float. All math is float (never double) with no libm, 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).