Compare commits

19 Commits

Author SHA1 Message Date
240d5cbd4b add slave response frame 2025-09-19 18:02:37 +02:00
3384c89475 add UDP command processing and sender for dynamic testing 2025-09-15 11:17:33 +02:00
8591e7433d truma_inetbox: refine master mode discovery with correct CP Plus signatures 2025-09-14 23:31:04 +02:00
4c097a576d truma_inetbox: enhance device discovery with targeted queries and logging 2025-09-14 23:00:01 +02:00
a0837070e2 add TriggerDiscoveryButton component with discovery functionality 2025-09-14 22:40:37 +02:00
958293f686 add CP Plus identifier for master mode in lin_identifier function 2025-09-14 22:05:27 +02:00
f022db25c4 truma_inetbox: enhance master mode device discovery with targeted queries 2025-09-14 21:22:22 +02:00
98ad6caab9 truma_inetbox: implement master mode auto-discovery and device scanning
udp_lin_receiver: add option to suppress repeated identical messages
2025-09-14 21:07:18 +02:00
aa5a85ec42 remove unused COMPONENT_VERSION constant 2025-09-14 16:55:28 +02:00
29235365dc rm test 2025-09-09 21:09:48 +02:00
ac8d902c08 print component version 2025-09-09 17:07:03 +02:00
451c184dda truma_inetbox(stream): stream slave responses too (enqueue in ISR, format/send in background); tag MASTER/SLAVE 2025-09-09 11:57:14 +02:00
7248f611df truma_inetbox(stream): decouple UDP streaming from logger level by streaming master frames from lin_msg queue 2025-09-09 11:54:25 +02:00
02cafc62dd truma_inetbox(stream): pin sender task to core 1; fix %s typo; keep keepalive/test queued; remove WiFi.h dependency 2025-09-09 11:29:15 +02:00
b5f02d0a47 truma_inetbox(stream): enqueue keepalive/test; add SO_SNDTIMEO on UDP socket 2025-09-09 10:53:37 +02:00
d0632b8457 truma_inetbox: fix ESP32 streaming preprocessor guards 2025-09-09 02:14:17 +02:00
382a4d3b4a truma_inetbox(stream): move UDP sending to background task
- Add FreeRTOS queue + dedicated sender task to avoid blocking main loop
- Short socket send timeout to prevent stalls
- Keepalive and PID lines enqueued; drop on full queue
- Add stream_send_test() to verify UDP path
2025-09-09 01:39:59 +02:00
99249dfaa9 truma_inetbox: add stream_send_test() helper to trigger a UDP test line 2025-09-08 20:02:05 +02:00
9e07fb6953 truma_inetbox: add UDP streaming (ESP32)
- observer_mode sniffer UDP: create socket, send 'PID XX <hex>' lines
- Keepalive with stream_keepalive_ms
- stream_diag_only filters to 0x3C/0x3D
- Controlled by stream_enabled/host/port
2025-09-08 17:11:53 +02:00
10 changed files with 576 additions and 9 deletions

View File

@@ -2,6 +2,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "helpers.h"
#ifdef USE_ESP32
#include <lwip/sockets.h>
#include <lwip/inet.h>
#include <sys/time.h>
#endif
namespace esphome {
namespace truma_inetbox {
@@ -57,6 +62,23 @@ void LinBusListener::setup() {
// call device specific function
this->setup_framework();
#ifdef USE_ESP32
if (this->stream_queue_ == nullptr) {
this->stream_queue_ = xQueueCreateStatic(
STREAM_QUEUE_LEN, STREAM_QUEUE_ITEM_MAX,
stream_static_queue_storage_, &stream_static_queue_);
}
if (this->streamTaskHandle_ == nullptr) {
xTaskCreatePinnedToCore(LinBusListener::streamTask_,
"lin_stream_task",
4096,
this,
1,
&this->streamTaskHandle_,
1); // pin to core 1 to avoid impacting WiFi on core 0
}
#endif
#if ESPHOME_LOG_LEVEL > ESPHOME_LOG_LEVEL_NONE
assert(this->log_queue_ != 0);
@@ -70,7 +92,15 @@ void LinBusListener::setup() {
}
}
void LinBusListener::update() { this->check_for_lin_fault_(); }
void LinBusListener::update() {
this->check_for_lin_fault_();
#ifdef USE_ESP32
if (this->stream_enabled_ && this->udp_sock_ < 0) {
this->stream_try_init_();
}
this->stream_maybe_keepalive_();
#endif
}
void LinBusListener::write_lin_answer_(const u_int8_t *data, u_int8_t len) {
QUEUE_LOG_MSG log_msg = QUEUE_LOG_MSG();
@@ -306,13 +336,14 @@ void LinBusListener::read_lin_frame_() {
TRUMA_LOGV_ISR(log_msg);
#endif // ESPHOME_LOG_HAS_VERBOSE
if (this->current_data_valid && message_from_master) {
if (this->current_data_valid) {
QUEUE_LIN_MSG lin_msg;
lin_msg.current_PID = this->current_PID_;
lin_msg.len = this->current_data_count_ - 1;
lin_msg.len = this->current_data_count_ - 1; // exclude CRC
for (u_int8_t i = 0; i < lin_msg.len; i++) {
lin_msg.data[i] = this->current_data_[i];
}
lin_msg.from_master = message_from_master ? 1 : 0;
xQueueSendFromISR(this->lin_msg_queue_, (void *) &lin_msg, QUEUE_WAIT_DONT_BLOCK);
}
this->current_state_ = READ_STATE_BREAK;
@@ -328,6 +359,10 @@ void LinBusListener::clear_uart_buffer_() {
void LinBusListener::process_lin_msg_queue(TickType_t xTicksToWait) {
QUEUE_LIN_MSG lin_msg;
while (xQueueReceive(this->lin_msg_queue_, &lin_msg, xTicksToWait) == pdPASS) {
#ifdef USE_ESP32
// Also forward master frames to UDP stream regardless of logger level
this->maybe_send_stream_from_lin_msg_(lin_msg);
#endif
this->lin_message_recieved_(lin_msg.current_PID, lin_msg.data, lin_msg.len);
}
}
@@ -337,6 +372,8 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
QUEUE_LOG_MSG log_msg;
while (xQueueReceive(this->log_queue_, &log_msg, xTicksToWait) == pdPASS) {
auto current_PID = log_msg.current_PID;
// Optional: forward to UDP stream before/after printing
this->maybe_send_stream_(log_msg);
switch (log_msg.type) {
case QUEUE_LOG_MSG_TYPE::ERROR_LIN_ANSWER_CAN_WRITE_LIN_ANSWER:
ESP_LOGE(TAG, "Cannot answer LIN because there is no open order from master.");
@@ -393,7 +430,7 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
log_msg.message_source_know ? (log_msg.message_from_master ? " - MASTER" : " - SLAVE") : "",
log_msg.current_data_valid ? "" : "INVALID");
} else {
ESP_LOGV(TAG, "PID %02X %s %s %S", current_PID_, format_hex_pretty(log_msg.data, log_msg.len).c_str(),
ESP_LOGV(TAG, "PID %02X %s %s %s", current_PID_, format_hex_pretty(log_msg.data, log_msg.len).c_str(),
log_msg.message_source_know ? (log_msg.message_from_master ? " - MASTER" : " - SLAVE") : "",
log_msg.current_data_valid ? "" : "INVALID");
}
@@ -405,6 +442,155 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
#endif // ESPHOME_LOG_LEVEL > ESPHOME_LOG_LEVEL_NONE
}
#ifdef USE_ESP32
void LinBusListener::maybe_send_stream_from_lin_msg_(const QUEUE_LIN_MSG &lin_msg) {
if (!this->stream_enabled_) return;
if (this->udp_sock_ < 0) this->stream_try_init_();
if (this->udp_sock_ < 0) return;
const uint8_t pid = lin_msg.current_PID;
if (this->stream_diag_only_ && !(pid == DIAGNOSTIC_FRAME_MASTER || pid == DIAGNOSTIC_FRAME_SLAVE)) {
return;
}
std::string line;
line.reserve(64);
char head[16];
snprintf(head, sizeof(head), "PID %02X ", pid);
line += head;
// Local hex format to avoid dependency on verbose-only helper
{
char b[4];
for (uint8_t i = 0; i < lin_msg.len; i++) {
if (!line.empty() && line.back() != ' ') line.push_back(' ');
snprintf(b, sizeof(b), "%02X", lin_msg.data[i]);
line += b;
}
}
line += (lin_msg.from_master ? " MASTER" : " SLAVE");
line.push_back('\n');
this->stream_enqueue_line_(line);
}
void LinBusListener::stream_send_test(const std::string &line) {
if (!this->stream_enabled_) return;
if (this->udp_sock_ < 0) this->stream_try_init_();
// Always enqueue to avoid blocking the main loop
std::string msg = line;
if (msg.empty()) msg = "STREAM TEST";
msg.push_back('\n');
this->stream_enqueue_line_(msg);
}
void LinBusListener::stream_try_init_() {
if (!this->stream_enabled_) return;
if (this->udp_sock_ >= 0) return;
if (this->udp_host_.empty() || this->udp_port_ == 0) {
ESP_LOGI(TAG, "UDP stream disabled: missing host/port");
return;
}
this->udp_sock_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (this->udp_sock_ < 0) {
ESP_LOGW(TAG, "UDP socket create failed");
return;
}
// Set a small send timeout so UDP sendto doesn't block long on ARP
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 50000; // 50ms
setsockopt(this->udp_sock_, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
memset(&this->udp_addr_, 0, sizeof(this->udp_addr_));
this->udp_addr_.sin_family = AF_INET;
this->udp_addr_.sin_port = htons(this->udp_port_);
this->udp_addr_.sin_addr.s_addr = inet_addr(this->udp_host_.c_str());
if (this->udp_addr_.sin_addr.s_addr == (in_addr_t) -1) {
ESP_LOGW(TAG, "UDP host invalid: %s", this->udp_host_.c_str());
::close(this->udp_sock_);
this->udp_sock_ = -1;
return;
}
ESP_LOGI(TAG, "UDP streaming to %s:%u", this->udp_host_.c_str(), (unsigned) this->udp_port_);
this->last_stream_send_ms_ = millis();
}
static std::string hex_bytes_(const uint8_t *data, uint8_t len) {
char buf[4];
std::string out;
out.reserve(len * 3);
for (uint8_t i = 0; i < len; i++) {
snprintf(buf, sizeof(buf), "%02X", data[i]);
if (!out.empty()) out.push_back(' ');
out += buf;
}
return out;
}
void LinBusListener::stream_maybe_keepalive_() {
if (!this->stream_enabled_) return;
const uint32_t now = millis();
if (this->stream_keepalive_ms_ == 0) return;
if (now - this->last_stream_send_ms_ >= this->stream_keepalive_ms_) {
// Enqueue keepalive to be sent from the background sender task
this->stream_enqueue_line_(std::string("KEEPALIVE\n"));
this->last_stream_send_ms_ = now;
}
}
void LinBusListener::maybe_send_stream_(const QUEUE_LOG_MSG &log_msg) {
if (!this->stream_enabled_) return;
if (this->udp_sock_ < 0) this->stream_try_init_();
if (this->udp_sock_ < 0) return;
const uint8_t pid = log_msg.current_PID;
if (this->stream_diag_only_ && !(pid == DIAGNOSTIC_FRAME_MASTER || pid == DIAGNOSTIC_FRAME_SLAVE)) {
return;
}
if (log_msg.len == 0) return;
// Compose a compact line: "PID XX <hex> [MASTER|SLAVE]"
std::string line;
line.reserve(64);
char head[16];
snprintf(head, sizeof(head), "PID %02X ", pid);
line += head;
line += hex_bytes_(log_msg.data, log_msg.len);
#ifdef ESPHOME_LOG_HAS_VERBOSE
if (log_msg.message_source_know) {
line += (log_msg.message_from_master ? " MASTER" : " SLAVE");
}
#endif
line.push_back('\n');
this->stream_enqueue_line_(line);
}
void LinBusListener::stream_enqueue_line_(const std::string &line) {
if (!this->stream_queue_) return;
if (line.empty()) return;
char buf[STREAM_QUEUE_ITEM_MAX];
size_t n = line.size();
if (n >= sizeof(buf)) n = sizeof(buf) - 1;
memcpy(buf, line.data(), n);
buf[n] = '\0';
xQueueSend(this->stream_queue_, buf, 0);
}
void LinBusListener::streamTask_(void *args) {
auto *self = static_cast<LinBusListener*>(args);
char buf[STREAM_QUEUE_ITEM_MAX];
for (;;) {
if (xQueueReceive(self->stream_queue_, buf, pdMS_TO_TICKS(100)) == pdPASS) {
if (self->udp_sock_ >= 0) {
size_t len = strnlen(buf, sizeof(buf));
::sendto(self->udp_sock_, buf, len, 0, (struct sockaddr *) &self->udp_addr_, sizeof(self->udp_addr_));
self->last_stream_send_ms_ = millis();
}
}
}
}
#else
void LinBusListener::maybe_send_stream_(const QUEUE_LOG_MSG &) {}
void LinBusListener::stream_try_init_() {}
void LinBusListener::stream_maybe_keepalive_() {}
void LinBusListener::stream_enqueue_line_(const std::string &) {}
void LinBusListener::streamTask_(void *) {}
#endif
#undef LIN_BREAK
#undef LIN_SYNC
#undef DIAGNOSTIC_FRAME_MASTER

View File

@@ -7,6 +7,8 @@
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>
#endif // USE_ESP32
#ifdef USE_RP2040
#include <hardware/uart.h>
@@ -31,6 +33,7 @@ struct QUEUE_LIN_MSG {
u_int8_t current_PID;
u_int8_t data[8];
u_int8_t len;
u_int8_t from_master; // 1 = master order, 0 = slave response
};
class LinBusListener : public PollingComponent, public uart::UARTDevice {
@@ -46,6 +49,8 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
void set_fault_pin(GPIOPin *pin) { this->fault_pin_ = pin; }
void set_observer_mode(bool val) { this->observer_mode_ = val; }
bool get_lin_bus_fault() { return fault_on_lin_bus_reported_ > 3; }
// Send a manual UDP test line (when streaming is enabled)
void stream_send_test(const std::string &line);
void process_lin_msg_queue(TickType_t xTicksToWait);
void process_log_queue(TickType_t xTicksToWait);
@@ -144,6 +149,10 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
void clear_uart_buffer_();
void setup_framework();
void maybe_send_stream_(const QUEUE_LOG_MSG &log_msg);
void stream_try_init_();
void stream_maybe_keepalive_();
void stream_enqueue_line_(const std::string &line);
void maybe_send_stream_from_lin_msg_(const QUEUE_LIN_MSG &lin_msg);
uint8_t lin_msg_static_queue_storage[TRUMA_MSG_QUEUE_LENGTH * sizeof(QUEUE_LIN_MSG)];
StaticQueue_t lin_msg_static_queue_;
@@ -164,6 +173,18 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
#ifdef USE_ESP32
TaskHandle_t eventTaskHandle_;
static void eventTask_(void *args);
// UDP streaming (ESP32 only)
int udp_sock_ = -1;
struct sockaddr_in udp_addr_ {};
uint32_t last_stream_send_ms_ = 0;
// Background sender task + queue to avoid blocking main loop
TaskHandle_t streamTaskHandle_ = nullptr;
static void streamTask_(void *args);
static constexpr size_t STREAM_QUEUE_ITEM_MAX = 120; // bytes per line
static constexpr size_t STREAM_QUEUE_LEN = 16;
uint8_t stream_static_queue_storage_[STREAM_QUEUE_LEN * STREAM_QUEUE_ITEM_MAX];
StaticQueue_t stream_static_queue_;
QueueHandle_t stream_queue_ = nullptr;
#endif // USE_ESP32
#ifdef USE_ESP32_FRAMEWORK_ESP_IDF
TaskHandle_t uartEventTaskHandle_;
@@ -176,4 +197,4 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
};
} // namespace truma_inetbox
} // namespace esphome
} // namespace esphome

View File

@@ -92,6 +92,8 @@ bool LinBusListener::write_lin_master_frame_(uint8_t pid, const uint8_t *data, u
auto uartComp = static_cast<ESPHOME_UART *>(this->parent_);
auto uart_num = uartComp->get_hw_serial_number();
auto hw_serial = uartComp->get_hw_serial();
// Send master frame (break + sync + pid + data + crc)
uart_send_break((uint8_t) uart_num);
hw_serial->write(0x55);
uint8_t pid_with_parity = (pid & 0x3F) | (addr_parity(pid) << 6);
@@ -100,6 +102,23 @@ bool LinBusListener::write_lin_master_frame_(uint8_t pid, const uint8_t *data, u
if (len > 0) hw_serial->write((uint8_t*)data, len);
hw_serial->write(crc);
hw_serial->flush();
// For diagnostic frames (0x3C), automatically schedule slave response (0x7D)
if (pid == 0x3C) {
// Small delay to allow slave processing time
delayMicroseconds(5000); // 5ms delay
// Send response slot header (break + sync + 0x7D with parity)
uart_send_break((uint8_t) uart_num);
hw_serial->write(0x55);
uint8_t response_pid_with_parity = (0x7D & 0x3F) | (addr_parity(0x7D) << 6);
hw_serial->write(response_pid_with_parity);
hw_serial->flush();
// Note: We don't send data/CRC for response slot - slave will provide that
ESP_LOGD(TAG, "Scheduled 0x7D response slot after 0x3C diagnostic frame");
}
return true;
}

View File

@@ -0,0 +1,19 @@
#include "TriggerDiscoveryButton.h"
#include "esphome/core/log.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.TriggerDiscoveryButton";
void TriggerDiscoveryButton::press_action() {
ESP_LOGI(TAG, "Discovery button pressed");
if (this->parent_ != nullptr) {
this->parent_->trigger_discovery();
} else {
ESP_LOGW(TAG, "No parent TrumaiNetBoxApp found");
}
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
#include "TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
class TriggerDiscoveryButton : public button::Button, public Component {
public:
void set_parent(TrumaiNetBoxApp *parent) { this->parent_ = parent; }
protected:
void press_action() override;
TrumaiNetBoxApp *parent_;
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -4,6 +4,14 @@
#include "esphome/core/helpers.h"
#include "helpers.h"
#ifdef USE_ESP32
#include <sstream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace esphome {
namespace truma_inetbox {
@@ -30,6 +38,28 @@ void TrumaiNetBoxApp::update() {
this->heater_.update();
this->timer_.update();
// Master mode auto-discovery (start device discovery once)
if (this->master_mode_ && !this->master_discovery_started_) {
this->master_discovery_started_ = true;
ESP_LOGI(TAG, "Master mode: Starting discovery with CORRECT CP Plus signature 17 46 10 03");
// Query identifier 0x23 with CORRECT CP Plus signature (17 46 10 03)
std::vector<uint8_t> query_23 = {0xB2, 0x23, 0x17, 0x46, 0x10, 0x03};
this->master_send_diag_single(0x7F, query_23);
this->master_send_diag_single(0x01, query_23); // Also try direct NAD
// Query identifier 0x00 with CORRECT CP Plus signature (17 46 10 03)
std::vector<uint8_t> query_00 = {0xB2, 0x00, 0x17, 0x46, 0x10, 0x03};
this->master_send_diag_single(0x7F, query_00);
this->master_send_diag_single(0x01, query_00); // Also try direct NAD
ESP_LOGI(TAG, "Master mode: Discovery sent - expecting heater response on PID 3D");
}
// UDP command receiver
this->process_udp_commands();
// Master TX scheduler (throttle to ~20ms spacing)
if (this->master_mode_ && !this->master_tx_queue_.empty()) {
uint32_t now = micros();
@@ -54,6 +84,12 @@ void TrumaiNetBoxApp::update() {
}
const std::array<uint8_t, 4> TrumaiNetBoxApp::lin_identifier() {
// In master mode, identify as CP Plus instead of iNet Box
if (this->master_mode_) {
// CP Plus Combi identifier - act as the LIN master controller
return {0x17 /*Supplied Id*/, 0x46 /*Supplied Id*/, 0x00 /*Function Id*/, 0x04 /*CP Plus*/};
}
// Supplier Id: 0x4617 - Truma (Phone: +49 (0)89 4617-0)
// Unknown:
// 17.46.01.03 - old Combi model
@@ -446,4 +482,122 @@ bool TrumaiNetBoxApp::master_scan_b2(uint8_t nad, uint8_t ident_start, uint8_t i
return true;
}
void TrumaiNetBoxApp::trigger_discovery() {
if (!this->master_mode_) {
ESP_LOGW(TAG, "Discovery can only be triggered in master mode");
return;
}
ESP_LOGI(TAG, "=== FIXED DISCOVERY SEQUENCE STARTED ===");
ESP_LOGI(TAG, "Using correct CP Plus signature 17 46 10 03 from UDP traffic analysis");
// Target heater NAD (seen responding as NAD 0x01 in traffic)
uint8_t heater_nad = 0x01;
// Also try broadcast for completeness
std::vector<uint8_t> nad_addresses = {0x7F, heater_nad};
for (uint8_t nad : nad_addresses) {
ESP_LOGI(TAG, "Sending discovery to NAD 0x%02X:", nad);
// 1. Query identifier 0x23 with CORRECT CP Plus signature (17 46 10 03)
std::vector<uint8_t> query_23 = {0xB2, 0x23, 0x17, 0x46, 0x10, 0x03};
this->master_send_diag_single(nad, query_23);
ESP_LOGI(TAG, " -> B2 23 17 46 10 03 (CP Plus sig)");
// 2. Query identifier 0x00 with CORRECT CP Plus signature (17 46 10 03)
std::vector<uint8_t> query_00 = {0xB2, 0x00, 0x17, 0x46, 0x10, 0x03};
this->master_send_diag_single(nad, query_00);
ESP_LOGI(TAG, " -> B2 00 17 46 10 03 (CP Plus sig)");
}
ESP_LOGI(TAG, "=== DISCOVERY COMPLETED - HEATER SHOULD RESPOND ON PID 3D ===");
}
void TrumaiNetBoxApp::process_udp_commands() {
if (!this->master_mode_) return;
// Initialize UDP command receiver socket on port 5556 (one port higher than stream)
if (this->udp_cmd_sock_ < 0 && !this->udp_cmd_init_attempted_) {
this->udp_cmd_init_attempted_ = true;
this->udp_cmd_sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (this->udp_cmd_sock_ < 0) {
ESP_LOGW(TAG, "UDP command socket creation failed");
return;
}
// Set non-blocking
int flags = fcntl(this->udp_cmd_sock_, F_GETFL, 0);
fcntl(this->udp_cmd_sock_, F_SETFL, flags | O_NONBLOCK);
// Bind to port 5556
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(5556);
if (bind(this->udp_cmd_sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
ESP_LOGW(TAG, "UDP command socket bind failed");
close(this->udp_cmd_sock_);
this->udp_cmd_sock_ = -1;
return;
}
ESP_LOGI(TAG, "UDP command receiver listening on port 5556");
ESP_LOGI(TAG, "Send commands like: 'CMD:B2 23 17 46 10 03 NAD:01'");
}
// Check for incoming commands
if (this->udp_cmd_sock_ >= 0) {
char buffer[256];
struct sockaddr_in sender_addr;
socklen_t addr_len = sizeof(sender_addr);
ssize_t len = recvfrom(this->udp_cmd_sock_, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&sender_addr, &addr_len);
if (len > 0) {
buffer[len] = '\0';
std::string cmd(buffer);
ESP_LOGI(TAG, "UDP Command received: %s", cmd.c_str());
// Parse command format: "CMD:B2 23 17 46 10 03 NAD:01"
size_t cmd_pos = cmd.find("CMD:");
size_t nad_pos = cmd.find("NAD:");
if (cmd_pos != std::string::npos && nad_pos != std::string::npos) {
std::string hex_data = cmd.substr(cmd_pos + 4, nad_pos - cmd_pos - 5);
std::string nad_str = cmd.substr(nad_pos + 4);
// Parse NAD
uint8_t nad = (uint8_t)strtoul(nad_str.c_str(), nullptr, 16);
// Parse hex bytes
std::vector<uint8_t> payload;
std::istringstream iss(hex_data);
std::string byte_str;
while (iss >> byte_str) {
if (byte_str.length() == 2) {
uint8_t byte_val = (uint8_t)strtoul(byte_str.c_str(), nullptr, 16);
payload.push_back(byte_val);
}
}
if (!payload.empty()) {
ESP_LOGI(TAG, "Sending diagnostic command to NAD 0x%02X with %zu bytes", nad, payload.size());
this->master_send_diag_single(nad, payload);
} else {
ESP_LOGW(TAG, "Failed to parse command payload");
}
} else {
ESP_LOGW(TAG, "Invalid command format. Use: CMD:B2 23 17 46 10 03 NAD:01");
}
}
}
}
} }

View File

@@ -31,6 +31,8 @@ class TrumaiNetBoxApp : public LinBusProtocol {
// Master scanner API
bool master_send_diag_single(uint8_t nad, const std::vector<uint8_t> &payload);
bool master_scan_b2(uint8_t nad, uint8_t ident_start, uint8_t ident_end );
void trigger_discovery();
void process_udp_commands();
TRUMA_DEVICE get_heater_device() const { return this->heater_device_; }
TRUMA_DEVICE get_aircon_device() const { return this->aircon_device_; }
@@ -93,6 +95,11 @@ class TrumaiNetBoxApp : public LinBusProtocol {
struct MasterReq { uint8_t pid; uint8_t len; uint8_t data[9]; };
std::queue<MasterReq> master_tx_queue_;
uint32_t last_master_send_us_ = 0;
bool master_discovery_started_ = false;
// UDP command receiver socket
int udp_cmd_sock_ = -1;
bool udp_cmd_init_attempted_ = false;
};

View File

@@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import button
from esphome.const import CONF_ID
from . import TrumaINetBoxApp, truma_inetbox_ns, CONF_TRUMA_INETBOX_ID
DEPENDENCIES = ["truma_inetbox"]
TriggerDiscoveryButton = truma_inetbox_ns.class_(
"TriggerDiscoveryButton", button.Button, cg.Component
)
CONFIG_SCHEMA = {
cv.GenerateID(): cv.declare_id(TriggerDiscoveryButton),
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
}
CONFIG_SCHEMA = button.button_schema(TriggerDiscoveryButton).extend(CONFIG_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await button.register_button(var, config)
await cg.register_component(var, config)
parent = await cg.get_variable(config[CONF_TRUMA_INETBOX_ID])
cg.add(var.set_parent(parent))

85
tools/udp_cmd_sender.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
UDP Command Sender for Truma ESP32 Master Mode
Sends diagnostic commands to ESP32 for dynamic testing
"""
import socket
import sys
import time
def send_command(esp_ip, command, nad):
"""Send a diagnostic command to ESP32"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Format: "CMD:B2 23 17 46 10 03 NAD:01"
message = f"CMD:{command} NAD:{nad:02X}"
print(f"Sending: {message}")
sock.sendto(message.encode(), (esp_ip, 5556))
sock.close()
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" python udp_cmd_sender.py <ESP32_IP> [command] [nad]")
print("")
print("Examples:")
print(" python udp_cmd_sender.py 192.168.1.90")
print(" python udp_cmd_sender.py 192.168.1.90 'B2 23 17 46 10 03' 01")
print(" python udp_cmd_sender.py 192.168.1.90 'B2 00 17 46 10 03' 01")
print(" python udp_cmd_sender.py 192.168.1.90 'B2 00 17 46 40 03' 01")
print("")
print("Interactive mode if no command specified.")
return
esp_ip = sys.argv[1]
if len(sys.argv) >= 4:
# Single command mode
command = sys.argv[2]
nad = int(sys.argv[3], 16)
send_command(esp_ip, command, nad)
else:
# Interactive mode
print(f"UDP Command Sender - Connected to ESP32 at {esp_ip}:5556")
print("Commands will be sent to ESP32 and you should see responses on UDP port 5555")
print("")
print("Useful commands to try:")
print(" B2 23 17 46 10 03 (Query identifier 0x23 with CP Plus signature)")
print(" B2 00 17 46 10 03 (Query identifier 0x00 with CP Plus signature)")
print(" B2 00 17 46 40 03 (Query identifier 0x00 with heater signature)")
print(" B2 00 17 46 01 03 (Query identifier 0x00 with old signature)")
print("")
print("Enter 'quit' to exit")
print("")
while True:
try:
user_input = input("Enter command (hex bytes): ").strip()
if user_input.lower() in ['quit', 'exit', 'q']:
break
if not user_input:
continue
# Get NAD
nad_input = input("Enter NAD (hex, default 01): ").strip()
if not nad_input:
nad = 1
else:
nad = int(nad_input, 16)
send_command(esp_ip, user_input, nad)
print("Command sent! Check UDP receiver for responses.")
print("")
except KeyboardInterrupt:
break
except Exception as e:
print(f"Error: {e}")
print("Goodbye!")
if __name__ == "__main__":
main()

View File

@@ -77,6 +77,7 @@ def run():
ap.add_argument("--duration", type=float, default=0.0, help="Stop after N seconds (0 = no limit)")
ap.add_argument("--quiet", action="store_true", help="No console output, only CSV")
ap.add_argument("--pid-diff", action="store_true", help="Track textual 'PID ..' lines and only print when data changes")
ap.add_argument("--suppress-repeats", action="store_true", help="Suppress repeated identical messages (shows first + count)")
ap.add_argument("--buffer", type=int, default=4096, help="Receive buffer size (default: 4096)")
args = ap.parse_args()
@@ -101,6 +102,9 @@ def run():
start = time.time()
last_by_pid: Dict[int, Tuple[int, ...]] = {}
last_message = None
repeat_count = 0
last_display_time = 0
while True:
if args.duration and (time.time() - start) >= args.duration:
@@ -129,6 +133,33 @@ def run():
# Console output
if not args.quiet:
# Format the display message
if is_text and as_text:
display_msg = f"{ts} {src_ip}:{src_port} ({size}B) TEXT: {as_text}"
else:
display_msg = f"{ts} {src_ip}:{src_port} ({size}B) HEX: {hexstr}"
# Handle repeat suppression
if args.suppress_repeats:
current_msg_key = as_text if is_text else hexstr
current_time = time.time()
if current_msg_key == last_message:
repeat_count += 1
# Show periodic updates for long repeated sequences
if current_time - last_display_time > 5.0: # Every 5 seconds
print(f"... repeated {repeat_count} times (last: {ts})")
last_display_time = current_time
else:
# Message changed
if repeat_count > 0:
print(f"... repeated {repeat_count} times total")
print(display_msg)
last_message = current_msg_key
repeat_count = 0
last_display_time = current_time
continue
if args.pid_diff and is_text:
parsed = maybe_parse_pid_and_data(as_text)
if parsed:
@@ -140,10 +171,7 @@ def run():
continue
# default console print
if is_text and as_text:
print(f"{ts} {src_ip}:{src_port} ({size}B) TEXT: {as_text}")
else:
print(f"{ts} {src_ip}:{src_port} ({size}B) HEX: {hexstr}")
print(display_msg)
if __name__ == "__main__":