Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to `cpp-sdk` are recorded here. Format follows [Keep a Chang

## [Unreleased]

### Changed

- `Property::setValue(float)` publishes the shortest decimal that reads back as the same float, laid out like python-sdk's float text: `21.4`, `40.0`, `1e-05`, `1e16` instead of `21.400000`. The formatters are `homie_format_float()` and `homie_format_double()` in `ebus/homie/homie_datatype.h`.

## [0.6.0] - 2026-10-10

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Version 0.6.0, alpha. Known limits:
- The POSIX demo build has no TLS; it connects to plain-TCP brokers only.
- After a broker loses its retained messages, esp32-sdk's reconnect does not restore `$description`; it does not yet call `Device::forgetDescriptionHash()` ([After a reconnect](doc/core.md#after-a-reconnect)).
- The driver contract (`NodeEntity`, `NodeProperty`) is still in esp32-sdk ([why](doc/core.md#still-in-esp32-sdk)).
- The code-first API has rough edges found while writing the POSIX demo: registering a settable property needs a live connection, the controller has no change callback, and float values are always formatted with `%f`.
- The code-first API has rough edges found while writing the POSIX demo: registering a settable property needs a live connection, and the controller has no change callback.
- esp32-sdk is not public yet; links to it will resolve once it is.

## Quick start: the POSIX demo
Expand Down
10 changes: 10 additions & 0 deletions homie/include/ebus/homie/homie_datatype.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,13 @@ bool homie_parse_number_format(const char* format, HomieNumberFormat* out);
// nothing in tree needs one, since a stepped format on a counter that large is not a
// configuration anyone writes.
bool homie_validate_number(double value, const char* format, double* coerced);

// Format a float payload: the shortest decimal that reads back as the same float, laid
// out like Python's repr() without the exponent '+' (fixed notation with at least one
// fraction digit for exponents -4..15, else d.ddde-XX), so "21.4", "40.0", "1e-05",
// "1e16". This is python-sdk's float text. Writes NUL-terminated text to out (n >= 32);
// a non-finite value is written as "nan", "inf" or "-inf", which is not a valid payload.
void homie_format_float(float value, char* out, size_t n);

// The same for a double: the shortest decimal that reads back as the same double.
void homie_format_double(double value, char* out, size_t n);
2 changes: 1 addition & 1 deletion homie/src/Property.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ void Property::setValue(int64_t value) {
}
void Property::setValue(float value) {
_floatValue = value;
snprintf(_value, sizeof(_value), "%f", value);
homie_format_float(value, _value, sizeof(_value));
_has_value = true;
}
void Property::setValue(const char* value) {
Expand Down
67 changes: 67 additions & 0 deletions homie/src/homie_datatype.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <ebus/homie/homie_datatype.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
Expand Down Expand Up @@ -288,3 +289,69 @@ bool homie_validate_duration(const char* payload) {
}
return any;
}

// --- float formatting ---------------------------------------------------------

// Shortest round trip: the fewest significant digits whose text reads back as `value`,
// through strtof() for a float (nine digits always suffice) or strtod() for a double
// (seventeen).
static void format_shortest(double value, bool is_float, char* out, size_t n) {
if (!isfinite(value)) {
snprintf(out, n, "%g", value);
return;
}
char sci[32];
const int max_prec = is_float ? 8 : 16;
for (int prec = 0; prec <= max_prec; ++prec) {
snprintf(sci, sizeof(sci), "%.*e", prec, value);
if (is_float ? strtof(sci, NULL) == (float)value : strtod(sci, NULL) == value) break;
}
// sci is "[-]d[.ddd]e(+|-)XX": split it into sign, digits and the decimal exponent.
const char* p = sci;
bool negative = (*p == '-');
if (negative) ++p;
char digits[20];
int ndigits = 0;
for (; *p != 'e'; ++p) {
if (*p != '.') digits[ndigits++] = *p;
}
int exp10 = atoi(p + 1);
while (ndigits > 1 && digits[ndigits - 1] == '0') --ndigits;

size_t len = 0;
char buf[48];
if (negative) buf[len++] = '-';
if (exp10 >= -4 && exp10 < 16) {
if (exp10 < 0) {
buf[len++] = '0';
buf[len++] = '.';
for (int i = -1; i > exp10; --i) buf[len++] = '0';
for (int i = 0; i < ndigits; ++i) buf[len++] = digits[i];
} else {
for (int i = 0; i <= exp10; ++i) buf[len++] = (i < ndigits) ? digits[i] : '0';
buf[len++] = '.';
if (ndigits > exp10 + 1) {
for (int i = exp10 + 1; i < ndigits; ++i) buf[len++] = digits[i];
} else {
buf[len++] = '0';
}
}
buf[len] = '\0';
} else {
buf[len++] = digits[0];
if (ndigits > 1) {
buf[len++] = '.';
for (int i = 1; i < ndigits; ++i) buf[len++] = digits[i];
}
snprintf(buf + len, sizeof(buf) - len, "e%s%02d", exp10 < 0 ? "-" : "", abs(exp10));
}
snprintf(out, n, "%s", buf);
}

void homie_format_float(float value, char* out, size_t n) {
format_shortest(value, true, out, n);
}

void homie_format_double(double value, char* out, size_t n) {
format_shortest(value, false, out, n);
}
57 changes: 57 additions & 0 deletions test/test_homie_datatype/test_homie_datatype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,57 @@ static void test_duration_repeated_component_is_rejected(void) {
TEST_ASSERT_FALSE(homie_validate_duration("PT1H2H"));
}


// ── float formatting ───────────────────────────────────────────────────────────────────

static void expect_float(float value, const char* text) {
char out[32];
homie_format_float(value, out, sizeof(out));
TEST_ASSERT_EQUAL_STRING(text, out);
}

static void test_float_is_formatted_with_the_shortest_round_trip_digits(void) {
expect_float(21.4f, "21.4");
expect_float(21.37f, "21.37");
expect_float(-3.5f, "-3.5");
expect_float(123456789.0f, "123456790.0");
}

static void test_whole_float_keeps_one_fraction_digit(void) {
expect_float(40.0f, "40.0");
expect_float(0.0f, "0.0");
expect_float(-0.0f, "-0.0");
}

static void test_float_uses_an_exponent_outside_1e_minus4_to_1e16(void) {
expect_float(0.0001f, "0.0001");
expect_float(1e-5f, "1e-05");
expect_float(1e15f, "1000000000000000.0");
expect_float(1e16f, "1e16");
expect_float(3.4028235e38f, "3.4028235e38");
}

static void test_double_is_formatted_with_the_shortest_round_trip_digits(void) {
char out[32];
homie_format_double(0.1, out, sizeof(out));
TEST_ASSERT_EQUAL_STRING("0.1", out);
homie_format_double(12345678.9, out, sizeof(out)); // more digits than a float holds
TEST_ASSERT_EQUAL_STRING("12345678.9", out);
homie_format_double(1e22, out, sizeof(out));
TEST_ASSERT_EQUAL_STRING("1e22", out);
}

static void test_formatted_float_is_a_valid_float_payload(void) {
const float values[] = {21.4f, 1e-5f, 1e16f, -0.0f, 1.17549435e-38f};
for (float v : values) {
char out[32];
double parsed = 0;
homie_format_float(v, out, sizeof(out));
TEST_ASSERT_TRUE_MESSAGE(homie_parse_float_payload(out, &parsed), out);
TEST_ASSERT_TRUE((float)parsed == v);
}
}

int main(int, char**) {
UNITY_BEGIN();

Expand Down Expand Up @@ -806,5 +857,11 @@ int main(int, char**) {
RUN_TEST(test_duration_components_out_of_order_are_rejected);
RUN_TEST(test_duration_repeated_component_is_rejected);


RUN_TEST(test_float_is_formatted_with_the_shortest_round_trip_digits);
RUN_TEST(test_whole_float_keeps_one_fraction_digit);
RUN_TEST(test_float_uses_an_exponent_outside_1e_minus4_to_1e16);
RUN_TEST(test_double_is_formatted_with_the_shortest_round_trip_digits);
RUN_TEST(test_formatted_float_is_a_valid_float_payload);
return UNITY_END();
}
2 changes: 1 addition & 1 deletion test/test_link_engine/test_link_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ static void test_local_retraction_keeps_the_last_good_value(void) {
temp.forget_value();
tick(l, 1000);
TEST_ASSERT_TRUE(l.source(0).has_value);
TEST_ASSERT_EQUAL_STRING("21.000000", l.source(0).value);
TEST_ASSERT_EQUAL_STRING("21.0", l.source(0).value);
TEST_ASSERT_EQUAL_INT(1, published(LINE_T));
}

Expand Down
2 changes: 1 addition & 1 deletion test/test_settable_dispatch/test_settable_dispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ static void test_target_is_published_first_with_the_exact_payload(void) {
TEST_ASSERT_EQUAL_STRING("40.2", t.at(0).payload);
TEST_ASSERT_TRUE(t.at(0).retained);
TEST_ASSERT_EQUAL_STRING(p->topic(), t.at(1).topic);
TEST_ASSERT_EQUAL_STRING("40.000000", t.at(1).payload); // stepped and coerced
TEST_ASSERT_EQUAL_STRING("40.0", t.at(1).payload); // stepped and coerced
}

static void test_refused_set_restores_the_value_and_publishes_nothing(void) {
Expand Down
Loading