Merge remote-tracking branch 'emsesp/dev' into dev

This commit is contained in:
MichaelDvP
2026-10-09 11:59:08 +02:00
23 changed files with 507 additions and 302 deletions
+127
View File
@@ -49,6 +49,18 @@ uint16_t EMSESP::wait_validate_ = 0;
bool EMSESP::wait_km_ = false;
uint32_t EMSESP::last_fetch_ = 0;
#ifdef EMSESP_GATEWAY_TX_HOLD
uint32_t EMSESP::gateway_hold_until_ = 0;
uint32_t EMSESP::gateway_hold_start_ = 0;
uint32_t EMSESP::gateway_cooldown_until_ = 0;
uint32_t EMSESP::gateway_window_start_ = 0;
uint8_t EMSESP::gateway_window_count_ = 0;
uint32_t EMSESP::gateway_fast_start_ = 0;
uint8_t EMSESP::gateway_fast_count_ = 0;
uint8_t EMSESP::last_version_request_src_ = 0;
uint32_t EMSESP::last_version_request_time_ = 0;
#endif
uint32_t EMSESP::last_entity_change_ = 0;
bool EMSESP::entity_compaction_pending_ = false;
@@ -1130,9 +1142,26 @@ void EMSESP::process_version(const std::shared_ptr<const Telegram> & telegram) {
brand = EMSdevice::Brand::NO_BRAND; // unknown
}
#ifdef EMSESP_GATEWAY_TX_HOLD
// skip the name request if we already know this device and its name
bool known = false;
for (const auto & emsdevice : emsdevices) {
if (emsdevice && emsdevice->is_device_id(device_id) && emsdevice->product_id() == product_id && !emsdevice->model().empty()) {
known = true;
break;
}
}
#endif
// add it - will be overwritten if device already exists
(void)add_device(device_id, product_id, version, brand);
#ifdef EMSESP_GATEWAY_TX_HOLD
if (known) {
return;
}
#endif
// request the deviceName from telegram 0x01
send_read_request(EMSdevice::EMS_TYPE_NAME, device_id, 27);
}
@@ -1249,7 +1278,16 @@ bool EMSESP::process_telegram(const std::shared_ptr<const Telegram> & telegram)
LOG_NOTICE("%s", pretty_telegram(telegram).c_str());
}
if (!wait_km_ && !found_device && (telegram->src != EMSbus::ems_bus_id()) && (telegram->message_length > 0)) {
#ifdef EMSESP_GATEWAY_TX_HOLD
// a new device often sends several broadcasts before its version reply is processed, only ask once
if (telegram->src != last_version_request_src_ || uuid::get_uptime() - last_version_request_time_ > 5000) {
last_version_request_src_ = telegram->src;
last_version_request_time_ = uuid::get_uptime();
send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src);
}
#else
send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src);
#endif
}
}
}
@@ -1511,6 +1549,13 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
// Print to LOG showing we've added a new device
LOG_INFO("Detected EMS device: %s (0x%02X)", EMSdevice::device_type_2_device_name(device_type), device_id);
#ifdef EMSESP_GATEWAY_TX_HOLD
// a gateway appearing while we're running is most likely starting up and about to sync
if (device_id == EMSdevice::EMS_DEVICE_ID_GATEWAY1) {
gateway_tx_hold_start();
}
#endif
// register the MQTT subscribe topic for this device
// except for controller and gateway
if ((device_type == DeviceType::CONTROLLER) || (device_type == DeviceType::GATEWAY)) {
@@ -1656,6 +1701,13 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
}
#endif
// check for poll to us, if so send top message from Tx queue immediately and quit
#ifdef EMSESP_GATEWAY_TX_HOLD
if (poll_id == EMSbus::ems_bus_id() && gateway_tx_hold()) {
// still ack the poll, otherwise the master drops us from 0x07 and the gateway sees the device list change
txservice_.send_poll();
return;
}
#endif
if (poll_id == txservice_.get_send_id()) {
txservice_.send();
} else {
@@ -1670,6 +1722,10 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
#endif
Roomctrl::check(data[1], data, length); // check if there is a message for the roomcontroller
#ifdef EMSESP_GATEWAY_TX_HOLD
gateway_rx_check(data, length);
#endif
rxservice_.add(data, length); // add to RxQueue
}
}
@@ -1677,6 +1733,11 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// fetch devices one by one
void EMSESP::scheduled_fetch_values() {
static uint8_t no = 0;
#ifdef EMSESP_GATEWAY_TX_HOLD
if (gateway_tx_hold()) {
return;
}
#endif
if (no || (uuid::get_uptime() - last_fetch_ > EMS_FETCH_FREQUENCY)) {
if (!no) {
last_fetch_ = uuid::get_uptime();
@@ -1698,6 +1759,72 @@ void EMSESP::scheduled_fetch_values() {
}
}
#ifdef EMSESP_GATEWAY_TX_HOLD
// count read requests from the gateway to other devices, a high rate means it's doing its startup sync
void EMSESP::gateway_rx_check(const uint8_t * data, const uint8_t length) {
if (length < 4 || (data[0] & 0x7F) != EMSdevice::EMS_DEVICE_ID_GATEWAY1 || !(data[1] & 0x80)) {
return;
}
uint8_t dest = data[1] & 0x7F;
if (dest == 0 || dest == EMSbus::ems_bus_id()) {
return;
}
uint32_t now = uuid::get_uptime();
if (now - gateway_window_start_ > GATEWAY_BURST_WINDOW) {
gateway_window_start_ = now;
gateway_window_count_ = 0;
}
if (now - gateway_fast_start_ > GATEWAY_FAST_WINDOW) {
gateway_fast_start_ = now;
gateway_fast_count_ = 0;
}
++gateway_window_count_;
++gateway_fast_count_;
if (gateway_window_count_ >= GATEWAY_BURST_THRESHOLD || gateway_fast_count_ >= GATEWAY_FAST_THRESHOLD) {
gateway_window_start_ = now;
gateway_window_count_ = 0;
gateway_fast_start_ = now;
gateway_fast_count_ = 0;
gateway_tx_hold_start();
}
}
void EMSESP::gateway_tx_hold_start() {
uint32_t now = uuid::get_uptime();
if (gateway_cooldown_until_) {
if ((int32_t)(now - gateway_cooldown_until_) < 0) {
return;
}
gateway_cooldown_until_ = 0;
}
if (!gateway_hold_until_) {
gateway_hold_start_ = now;
LOG_INFO("Gateway 0x%02X is busy, holding Tx", EMSdevice::EMS_DEVICE_ID_GATEWAY1);
}
gateway_hold_until_ = (now + GATEWAY_HOLD_TIME) | 1; // 0 means no hold
}
bool EMSESP::gateway_tx_hold() {
if (!gateway_hold_until_) {
return false;
}
uint32_t now = uuid::get_uptime();
if (now - gateway_hold_start_ > GATEWAY_HOLD_MAX) {
gateway_hold_until_ = 0;
gateway_cooldown_until_ = (now + GATEWAY_HOLD_COOLDOWN) | 1;
LOG_WARNING("Gateway 0x%02X still busy after %lu s, resuming Tx", EMSdevice::EMS_DEVICE_ID_GATEWAY1, (unsigned long)(GATEWAY_HOLD_MAX / 1000));
return false;
}
if ((int32_t)(now - gateway_hold_until_) >= 0) {
gateway_hold_until_ = 0;
LOG_INFO("Gateway 0x%02X is quiet, resuming Tx after %lu s", EMSdevice::EMS_DEVICE_ID_GATEWAY1, (unsigned long)((now - gateway_hold_start_) / 1000));
return false;
}
return true;
}
#endif
// EMSESP main class
EMSESP::EMSESP()
#ifndef EMSESP_STANDALONE
+26
View File
@@ -291,6 +291,32 @@ class EMSESP {
static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute
static constexpr uint8_t EMS_WAIT_KM_TIMEOUT = 60; // wait one minute
#ifdef EMSESP_GATEWAY_TX_HOLD
// hold our Tx while a gateway (K30RF/KM200 on 0x48) is doing its startup sync, see issue #3094
// a startup sync is 55-250 read requests/min to other devices, normal operation is <10/min
static void gateway_rx_check(const uint8_t * data, const uint8_t length);
static bool gateway_tx_hold();
static void gateway_tx_hold_start();
static constexpr uint32_t GATEWAY_BURST_WINDOW = 30000; // ms window for counting gateway read requests
static constexpr uint8_t GATEWAY_BURST_THRESHOLD = 15; // read requests within window to treat gateway as busy
static constexpr uint32_t GATEWAY_FAST_WINDOW = 5000; // short window to catch the start of a burst quickly
static constexpr uint8_t GATEWAY_FAST_THRESHOLD = 8; // normal polling is ~1 read per 2s, a sync starts at >10/s
static constexpr uint32_t GATEWAY_HOLD_TIME = 60000; // keep holding Tx this long after the last busy window
static constexpr uint32_t GATEWAY_HOLD_MAX = 600000; // never hold longer than 10 minutes in one go
static constexpr uint32_t GATEWAY_HOLD_COOLDOWN = 600000; // after hitting the max, don't hold again for 10 minutes
static uint32_t gateway_hold_until_;
static uint32_t gateway_hold_start_;
static uint32_t gateway_cooldown_until_;
static uint32_t gateway_window_start_;
static uint8_t gateway_window_count_;
static uint32_t gateway_fast_start_;
static uint8_t gateway_fast_count_;
static uint8_t last_version_request_src_;
static uint32_t last_version_request_time_;
#endif
static constexpr uint32_t SYSTEM_STATUS_RESET_TIMEOUT = 300000; // return a non-normal system status to normal after this (in ms) - 5 minutes
struct Device_record {
+3 -2
View File
@@ -1318,8 +1318,9 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
break;
case DeviceValueUOM::MV:
case DeviceValueUOM::VOLTS:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "voltage";
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "voltage";
doc["sug_dsp_prc"] = 2; // round to 2 decimal places
break;
case DeviceValueUOM::MBAR:
doc[sc_ha] = sc_ha_measurement;
+10
View File
@@ -558,6 +558,16 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
// send a Tx telegram to request data from an EMS device
void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, const uint8_t length, const bool front) {
#ifdef EMSESP_GATEWAY_TX_HOLD
// while Tx is held the queue isn't drained, so periodic triggers (e.g. UBADevices) would stack up identical reads
for (const auto & queued : tx_telegrams_) {
const auto & t = queued.telegram_;
if (t->operation == Telegram::Operation::TX_READ && t->dest == dest && t->type_id == type_id && t->offset == offset) {
LOG_DEBUG("Tx read request to deviceID 0x%02X for typeID 0x%02X already queued", dest, type_id);
return;
}
}
#endif
LOG_DEBUG("Tx read request to deviceID 0x%02X for typeID 0x%02X", dest, type_id);
uint8_t message_data = 0xFF;
+27 -21
View File
@@ -182,9 +182,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
register_telegram_type(set_typeids[i], "RC300Set", false, MAKE_PF_CB(process_RC300Set), 29);
register_telegram_type(summer_typeids[i], "RC300Summer", false, MAKE_PF_CB(process_RC300Summer), 14);
register_telegram_type(curve_typeids[i], "RC300Curves", false, MAKE_PF_CB(process_RC300Curve), 9);
if (model != EMSdevice::EMS_DEVICE_FLAG_UI800) {
register_telegram_type(summer2_typeids[i], "RC300Summer2", false, MAKE_PF_CB(process_RC300Summer2), 8);
}
register_telegram_type(summer2_typeids[i], "RC300Summer2", false, MAKE_PF_CB(process_RC300Summer2), 8);
}
const size_t set2_size = set2_typeids.size();
for (uint8_t i = 0; i < set2_size; i++) {
@@ -1183,7 +1181,12 @@ void Thermostat::process_RC300Monitor(const std::shared_ptr<const Telegram> & te
has_update(hc->hpoperatingstate, EMS_VALUE_UINT8_NOTSET);
} else {
has_enumupdate(telegram, hc->hpoperatingstate, 20, 1); // 1:heating, 2:off, 3:cooling
has_update(hc->summermode, EMS_VALUE_UINT8_NOTSET);
// UI800 heatpumps report a valid summer bit, older RC300 heatpumps don't, #3257
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
has_update(hc->summermode, hc->statusbyte & 0x50 ? 1 : 0);
} else {
has_update(hc->summermode, EMS_VALUE_UINT8_NOTSET);
}
}
has_update(telegram, hc->targetflowtemp, 4);
has_update(telegram, hc->curroominfl, 27);
@@ -1257,7 +1260,7 @@ void Thermostat::process_RC300Summer(const std::shared_ptr<const Telegram> & tel
has_update(telegram, hc->roominfl_factor, 1); // is * 10
has_update(telegram, hc->offsettemp, 2);
has_update(telegram, hc->solarInfl, 3);
if (!is_received(summer2_typeids[hc->hc()])) {
if (!is_received(summer2_typeids[hc->hc()]) && !has_summer3()) {
has_update(telegram, hc->summertemp, 6);
has_update(telegram, hc->summersetmode, 7);
}
@@ -1275,6 +1278,11 @@ void Thermostat::process_RC300Summer(const std::shared_ptr<const Telegram> & tel
has_update(telegram, hc->comfortPointTemp, 12);
}
// some UI800 (e.g. HMI800.2) don't send 0x470 and use the per-hc 0x471 ff instead, #3257
bool Thermostat::has_summer3() const {
return model() == EMSdevice::EMS_DEVICE_FLAG_UI800 && is_received(0x470);
}
// type 0x470, only BC400/UI800
// (0x470), data: 01 0E 01 01 02 17 04 48
void Thermostat::process_RC300Summer3(const std::shared_ptr<const Telegram> & telegram) {
@@ -2655,7 +2663,7 @@ bool Thermostat::set_coolondelay(const char * value, const int8_t id) {
if (!Helpers::value2float(value, f)) {
return false;
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 6, (uint8_t)(f * 4), 0x470);
return true;
}
@@ -2672,7 +2680,7 @@ bool Thermostat::set_cooloffdelay(const char * value, const int8_t id) {
if (!Helpers::value2float(value, f)) {
return false;
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 7, (uint8_t)(f * 4), 0x470);
return true;
}
@@ -3439,7 +3447,7 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) {
// sets the thermostat summermode for RC300
bool Thermostat::set_summermode(const char * value, const int8_t id) {
uint8_t set;
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
if (Helpers::value2enum(value, set, FL_(enum_summermode))) {
write_command(0x470, 0, set, 0x470);
return true;
@@ -3600,7 +3608,7 @@ bool Thermostat::set_heatondelay(const char * value, const int8_t id) {
if (!Helpers::value2float(value, f)) {
return false;
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 2, (uint8_t)(f * 4), 0x470);
return true;
}
@@ -3617,7 +3625,7 @@ bool Thermostat::set_heatoffdelay(const char * value, const int8_t id) {
if (!Helpers::value2float(value, f)) {
return false;
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 3, (uint8_t)(f * 4), 0x470);
return true;
}
@@ -3634,7 +3642,7 @@ bool Thermostat::set_instantstart(const char * value, const int8_t id) {
if (!Helpers::value2number(value, v)) {
return false;
}
if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 4, (uint8_t)v, 0x470);
return true;
}
@@ -4164,7 +4172,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co
validate_typeid = set_typeids[hc->hc()];
switch (mode) {
case HeatingCircuit::Mode::SUMMER:
if (model == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 1, temperature, 0x470);
return true;
}
@@ -4179,7 +4187,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co
factor = 1;
break;
case HeatingCircuit::Mode::COOLSTART:
if (model == EMSdevice::EMS_DEVICE_FLAG_UI800) {
if (has_summer3()) {
write_command(0x470, 5, temperature, 0x470);
return true;
}
@@ -5207,14 +5215,12 @@ void Thermostat::register_device_values_hc(std::shared_ptr<Thermostat::HeatingCi
101);
register_device_value(tag, &hc->remotehum, DeviceValueType::CMD, FL_(remotehum), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_remotehum), -1, 101);
}
if (model != EMSdevice::EMS_DEVICE_FLAG_UI800) {
register_device_value(tag, &hc->heatondelay, DeviceValueType::UINT8, FL_(heatondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatondelay), 1, 48);
register_device_value(tag, &hc->heatoffdelay, DeviceValueType::UINT8, FL_(heatoffdelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatoffdelay), 1, 48);
register_device_value(tag, &hc->coolondelay, DeviceValueType::UINT8, FL_(coolondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_coolondelay), 1, 48);
register_device_value(tag, &hc->cooloffdelay, DeviceValueType::UINT8, FL_(cooloffdelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_cooloffdelay), 1, 48);
register_device_value(tag, &hc->instantstart, DeviceValueType::UINT8, FL_(instantstart), DeviceValueUOM::K, MAKE_CF_CB(set_instantstart), 1, 10);
register_device_value(tag, &hc->coolstart, DeviceValueType::UINT8, FL_(coolstart), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_coolstart), 20, 35);
}
register_device_value(tag, &hc->heatondelay, DeviceValueType::UINT8, FL_(heatondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatondelay), 1, 48);
register_device_value(tag, &hc->heatoffdelay, DeviceValueType::UINT8, FL_(heatoffdelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatoffdelay), 1, 48);
register_device_value(tag, &hc->coolondelay, DeviceValueType::UINT8, FL_(coolondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_coolondelay), 1, 48);
register_device_value(tag, &hc->cooloffdelay, DeviceValueType::UINT8, FL_(cooloffdelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_cooloffdelay), 1, 48);
register_device_value(tag, &hc->instantstart, DeviceValueType::UINT8, FL_(instantstart), DeviceValueUOM::K, MAKE_CF_CB(set_instantstart), 1, 10);
register_device_value(tag, &hc->coolstart, DeviceValueType::UINT8, FL_(coolstart), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_coolstart), 20, 35);
register_device_value(tag, &hc->boost, DeviceValueType::BOOL, FL_(boost), DeviceValueUOM::NONE, MAKE_CF_CB(set_boost));
register_device_value(tag, &hc->boosttime, DeviceValueType::UINT8, FL_(boosttime), DeviceValueUOM::HOURS, MAKE_CF_CB(set_boosttime));
register_device_value(tag,
+3
View File
@@ -459,9 +459,12 @@ class Thermostat : public EMSdevice {
void process_RC300Monitor(const std::shared_ptr<const Telegram> & telegram);
void process_RC300Set(const std::shared_ptr<const Telegram> & telegram);
void process_RC300Set2(const std::shared_ptr<const Telegram> & telegram);
void process_RC300Summer(const std::shared_ptr<const Telegram> & telegram);
void process_RC300Summer2(const std::shared_ptr<const Telegram> & telegram);
void process_RC300Summer3(const std::shared_ptr<const Telegram> & telegram);
bool has_summer3() const;
void process_RC300WWmode(const std::shared_ptr<const Telegram> & telegram);
void process_RC300WWmode2(const std::shared_ptr<const Telegram> & telegram);
void process_RC300WWtemp(const std::shared_ptr<const Telegram> & telegram);
+1 -1
View File
@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.15"
#define EMSESP_APP_VERSION "3.9.0-dev.16"
+8
View File
@@ -24,6 +24,7 @@
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "soc/uart_reg.h"
#include "uart/emsuart_esp32.h"
#include "emsesp.h"
@@ -91,6 +92,7 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t
#endif
uart_param_config(EMSUART_NUM, &uart_config);
uart_set_pin(EMSUART_NUM, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
gpio_pullup_en((gpio_num_t)rx_gpio); // IDF 5 no longer enables the Rx pull-up, some custom interfaces rely on it
uart_set_line_inverse(EMSUART_NUM, inverse_mask);
uart_driver_install(EMSUART_NUM,
UART_HW_FIFO_LEN(EMSUART_NUM) + 1,
@@ -109,6 +111,7 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t
#endif
} else {
uart_set_pin(EMSUART_NUM, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
gpio_pullup_en((gpio_num_t)rx_gpio);
vTaskResume(xHandle);
}
tx_mode_ = tx_mode;
@@ -153,6 +156,11 @@ uint8_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
last_tx_src_ = len < 4 ? 0 : buf[0]; // update last tx source
#ifdef EMSESP_TX_PRE_DELAY
// the BRK event fires after one character time of low, while the master break may still be running
delayMicroseconds(EMSESP_TX_PRE_DELAY);
#endif
// TXMODE is hardware controlled mode
if (tx_mode_ == EMS_TXMODE_HW) {
uart_write_bytes_with_break(EMSUART_NUM, buf, len, 10);
+6 -5
View File
@@ -156,14 +156,15 @@ void WebLogService::loop() {
// convert time to real offset
char * WebLogService::messagetime(char * out, const uint64_t t, const size_t bufsize) {
time_t offset = time(nullptr) - uuid::get_uptime_sec();
if (offset < 1500000000L) {
int64_t offset_ms = uuid::get_wall_offset_ms();
if (offset_ms < 1500000000000LL) {
strlcpy(out, uuid::log::format_timestamp_ms(t, 3).c_str(), bufsize);
} else {
time_t t1 = offset + (time_t)(t / 1000);
char timestr[25];
int64_t wall_ms = offset_ms + (int64_t)t;
time_t t1 = (time_t)(wall_ms / 1000);
char timestr[25];
strftime(timestr, 25, "%FT%T", localtime(&t1));
snprintf(out, bufsize, "%s.%03d", timestr, (uint16_t)(t % 1000));
snprintf(out, bufsize, "%s.%03d", timestr, (int)(wall_ms % 1000));
}
return out;
}