2 Commits

9 changed files with 27 additions and 526 deletions

View File

@@ -2,11 +2,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "helpers.h" #include "helpers.h"
#ifdef USE_ESP32
#include <lwip/sockets.h>
#include <lwip/inet.h>
#include <sys/time.h>
#endif
namespace esphome { namespace esphome {
namespace truma_inetbox { namespace truma_inetbox {
@@ -62,23 +57,6 @@ void LinBusListener::setup() {
// call device specific function // call device specific function
this->setup_framework(); 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 #if ESPHOME_LOG_LEVEL > ESPHOME_LOG_LEVEL_NONE
assert(this->log_queue_ != 0); assert(this->log_queue_ != 0);
@@ -92,15 +70,7 @@ void LinBusListener::setup() {
} }
} }
void LinBusListener::update() { void LinBusListener::update() { this->check_for_lin_fault_(); }
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) { void LinBusListener::write_lin_answer_(const u_int8_t *data, u_int8_t len) {
QUEUE_LOG_MSG log_msg = QUEUE_LOG_MSG(); QUEUE_LOG_MSG log_msg = QUEUE_LOG_MSG();
@@ -336,14 +306,13 @@ void LinBusListener::read_lin_frame_() {
TRUMA_LOGV_ISR(log_msg); TRUMA_LOGV_ISR(log_msg);
#endif // ESPHOME_LOG_HAS_VERBOSE #endif // ESPHOME_LOG_HAS_VERBOSE
if (this->current_data_valid) { if (this->current_data_valid && message_from_master) {
QUEUE_LIN_MSG lin_msg; QUEUE_LIN_MSG lin_msg;
lin_msg.current_PID = this->current_PID_; lin_msg.current_PID = this->current_PID_;
lin_msg.len = this->current_data_count_ - 1; // exclude CRC lin_msg.len = this->current_data_count_ - 1;
for (u_int8_t i = 0; i < lin_msg.len; i++) { for (u_int8_t i = 0; i < lin_msg.len; i++) {
lin_msg.data[i] = this->current_data_[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); xQueueSendFromISR(this->lin_msg_queue_, (void *) &lin_msg, QUEUE_WAIT_DONT_BLOCK);
} }
this->current_state_ = READ_STATE_BREAK; this->current_state_ = READ_STATE_BREAK;
@@ -359,10 +328,6 @@ void LinBusListener::clear_uart_buffer_() {
void LinBusListener::process_lin_msg_queue(TickType_t xTicksToWait) { void LinBusListener::process_lin_msg_queue(TickType_t xTicksToWait) {
QUEUE_LIN_MSG lin_msg; QUEUE_LIN_MSG lin_msg;
while (xQueueReceive(this->lin_msg_queue_, &lin_msg, xTicksToWait) == pdPASS) { 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); this->lin_message_recieved_(lin_msg.current_PID, lin_msg.data, lin_msg.len);
} }
} }
@@ -372,8 +337,6 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
QUEUE_LOG_MSG log_msg; QUEUE_LOG_MSG log_msg;
while (xQueueReceive(this->log_queue_, &log_msg, xTicksToWait) == pdPASS) { while (xQueueReceive(this->log_queue_, &log_msg, xTicksToWait) == pdPASS) {
auto current_PID = log_msg.current_PID; 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) { switch (log_msg.type) {
case QUEUE_LOG_MSG_TYPE::ERROR_LIN_ANSWER_CAN_WRITE_LIN_ANSWER: 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."); ESP_LOGE(TAG, "Cannot answer LIN because there is no open order from master.");
@@ -430,7 +393,7 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
log_msg.message_source_know ? (log_msg.message_from_master ? " - MASTER" : " - SLAVE") : "", log_msg.message_source_know ? (log_msg.message_from_master ? " - MASTER" : " - SLAVE") : "",
log_msg.current_data_valid ? "" : "INVALID"); log_msg.current_data_valid ? "" : "INVALID");
} else { } 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.message_source_know ? (log_msg.message_from_master ? " - MASTER" : " - SLAVE") : "",
log_msg.current_data_valid ? "" : "INVALID"); log_msg.current_data_valid ? "" : "INVALID");
} }
@@ -442,155 +405,6 @@ void LinBusListener::process_log_queue(TickType_t xTicksToWait) {
#endif // ESPHOME_LOG_LEVEL > ESPHOME_LOG_LEVEL_NONE #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_BREAK
#undef LIN_SYNC #undef LIN_SYNC
#undef DIAGNOSTIC_FRAME_MASTER #undef DIAGNOSTIC_FRAME_MASTER

View File

@@ -7,8 +7,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_RP2040 #ifdef USE_RP2040
#include <hardware/uart.h> #include <hardware/uart.h>
@@ -33,7 +31,6 @@ struct QUEUE_LIN_MSG {
u_int8_t current_PID; u_int8_t current_PID;
u_int8_t data[8]; u_int8_t data[8];
u_int8_t len; u_int8_t len;
u_int8_t from_master; // 1 = master order, 0 = slave response
}; };
class LinBusListener : public PollingComponent, public uart::UARTDevice { class LinBusListener : public PollingComponent, public uart::UARTDevice {
@@ -49,51 +46,21 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
void set_fault_pin(GPIOPin *pin) { this->fault_pin_ = pin; } void set_fault_pin(GPIOPin *pin) { this->fault_pin_ = pin; }
void set_observer_mode(bool val) { this->observer_mode_ = val; } void set_observer_mode(bool val) { this->observer_mode_ = val; }
bool get_lin_bus_fault() { return fault_on_lin_bus_reported_ > 3; } 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_lin_msg_queue(TickType_t xTicksToWait);
void process_log_queue(TickType_t xTicksToWait); void process_log_queue(TickType_t xTicksToWait);
// Streaming configuration (UDP)
void set_stream_enabled(bool val) { this->stream_enabled_ = val; }
void set_stream_diag_only(bool val) { this->stream_diag_only_ = val; }
void set_stream_keepalive_ms(uint32_t val) { this->stream_keepalive_ms_ = val; }
void set_udp_stream_host(const std::string &host) { this->udp_host_ = host; }
void set_udp_stream_port(uint16_t port) { this->udp_port_ = port; }
// Master (LIN initiator) configuration
void set_master_mode(bool val) { this->master_mode_ = val; }
void set_master_nad(uint8_t nad) { this->master_nad_ = nad; }
void set_writes_armed(bool val) { this->writes_armed_ = val; }
#ifdef USE_RP2040 #ifdef USE_RP2040
// Return is the expected wait time till next data check is recommended. // Return is the expected wait time till next data check is recommended.
u_int32_t onSerialEvent(); u_int32_t onSerialEvent();
#endif // USE_RP2040 #endif // USE_RP2040
protected: protected:
// Low-level: send a LIN master frame (break + sync + pid + data + crc)
bool write_lin_master_frame_(uint8_t pid, const uint8_t *data, uint8_t len);
LIN_CHECKSUM lin_checksum_ = LIN_CHECKSUM::LIN_CHECKSUM_VERSION_2; LIN_CHECKSUM lin_checksum_ = LIN_CHECKSUM::LIN_CHECKSUM_VERSION_2;
GPIOPin *cs_pin_ = nullptr; GPIOPin *cs_pin_ = nullptr;
GPIOPin *fault_pin_ = nullptr; GPIOPin *fault_pin_ = nullptr;
bool observer_mode_ = false; bool observer_mode_ = false;
// Streaming config (UDP)
bool stream_enabled_ = false;
bool stream_diag_only_ = false;
uint32_t stream_keepalive_ms_ = 1000;
std::string udp_host_ = {};
uint16_t udp_port_ = 0;
// Master (LIN initiator) config
bool master_mode_ = false;
uint8_t master_nad_ = 0x00;
bool writes_armed_ = false;
void write_lin_answer_(const u_int8_t *data, u_int8_t len); void write_lin_answer_(const u_int8_t *data, u_int8_t len);
bool check_for_lin_fault_(); bool check_for_lin_fault_();
virtual bool answer_lin_order_(const u_int8_t pid) = 0; virtual bool answer_lin_order_(const u_int8_t pid) = 0;
@@ -148,11 +115,6 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
void read_lin_frame_(); void read_lin_frame_();
void clear_uart_buffer_(); void clear_uart_buffer_();
void setup_framework(); 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)]; uint8_t lin_msg_static_queue_storage[TRUMA_MSG_QUEUE_LENGTH * sizeof(QUEUE_LIN_MSG)];
StaticQueue_t lin_msg_static_queue_; StaticQueue_t lin_msg_static_queue_;
@@ -173,18 +135,6 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
#ifdef USE_ESP32 #ifdef USE_ESP32
TaskHandle_t eventTaskHandle_; TaskHandle_t eventTaskHandle_;
static void eventTask_(void *args); 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 #endif // USE_ESP32
#ifdef USE_ESP32_FRAMEWORK_ESP_IDF #ifdef USE_ESP32_FRAMEWORK_ESP_IDF
TaskHandle_t uartEventTaskHandle_; TaskHandle_t uartEventTaskHandle_;
@@ -197,4 +147,4 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
}; };
} // namespace truma_inetbox } // namespace truma_inetbox
} // namespace esphome } // namespace esphome

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "LinBusListener.h" #include "LinBusListener.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "driver/uart.h" #include "driver/uart.h"
@@ -12,17 +12,9 @@
#endif // CUSTOM_ESPHOME_UART #endif // CUSTOM_ESPHOME_UART
#include "esphome/components/uart/uart_component_esp32_arduino.h" #include "esphome/components/uart/uart_component_esp32_arduino.h"
// Forward declarations for diag helpers
extern u_int8_t addr_parity(const u_int8_t pid);
extern u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum);
namespace esphome { namespace esphome {
namespace truma_inetbox { namespace truma_inetbox {
// Prototypes for helper functions in this namespace
u_int8_t addr_parity(const u_int8_t pid);
u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum);
static const char *const TAG = "truma_inetbox.LinBusListener"; static const char *const TAG = "truma_inetbox.LinBusListener";
#define QUEUE_WAIT_BLOCKING (portTickType) portMAX_DELAY #define QUEUE_WAIT_BLOCKING (portTickType) portMAX_DELAY
@@ -47,7 +39,7 @@ void LinBusListener::setup_framework() {
uart_intr.rx_timeout_thresh = uart_intr.rx_timeout_thresh =
10; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive 10; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive
uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT
uart_intr_config((uart_port_t) uart_num, &uart_intr); uart_intr_config(uart_num, &uart_intr);
hw_serial->onReceive([this]() { this->onReceive_(); }, false); hw_serial->onReceive([this]() { this->onReceive_(); }, false);
hw_serial->onReceiveError([this](hardwareSerial_error_t val) { hw_serial->onReceiveError([this](hardwareSerial_error_t val) {
@@ -85,28 +77,10 @@ void LinBusListener::eventTask_(void *args) {
} }
} }
bool LinBusListener::write_lin_master_frame_(uint8_t pid, const uint8_t *data, uint8_t len) {
if (!this->master_mode_) return false;
if (len > 8) return false;
auto uartComp = static_cast<ESPHOME_UART *>(this->parent_);
auto uart_num = uartComp->get_hw_serial_number();
auto hw_serial = uartComp->get_hw_serial();
uart_send_break((uint8_t) uart_num);
hw_serial->write(0x55);
uint8_t pid_with_parity = (pid & 0x3F) | (addr_parity(pid) << 6);
hw_serial->write(pid_with_parity);
uint8_t crc = data_checksum(data, len, 0);
if (len > 0) hw_serial->write((uint8_t*)data, len);
hw_serial->write(crc);
hw_serial->flush();
return true;
}
} // namespace truma_inetbox } // namespace truma_inetbox
} // namespace esphome } // namespace esphome
#undef QUEUE_WAIT_BLOCKING #undef QUEUE_WAIT_BLOCKING
#undef ESPHOME_UART #undef ESPHOME_UART
#endif // USE_ESP32_FRAMEWORK_ARDUINO #endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -15,7 +15,7 @@ namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.LinBusListener"; static const char *const TAG = "truma_inetbox.LinBusListener";
#define QUEUE_WAIT_BLOCKING portMAX_DELAY #define QUEUE_WAIT_BLOCKING (TickType_t) portMAX_DELAY
void LinBusListener::setup_framework() { void LinBusListener::setup_framework() {
// uartSetFastReading // uartSetFastReading

View File

@@ -66,9 +66,9 @@ enum class QUEUE_LOG_MSG_TYPE {
// Log messages generated during interrupt are pushed to log queue. // Log messages generated during interrupt are pushed to log queue.
struct QUEUE_LOG_MSG { struct QUEUE_LOG_MSG {
QUEUE_LOG_MSG_TYPE type; QUEUE_LOG_MSG_TYPE type;
uint8_t current_PID; u_int8_t current_PID;
uint8_t data[9]; u_int8_t data[9];
uint8_t len; u_int8_t len;
#ifdef ESPHOME_LOG_HAS_VERBOSE #ifdef ESPHOME_LOG_HAS_VERBOSE
bool current_data_valid; bool current_data_valid;
bool message_source_know; bool message_source_know;

View File

@@ -30,15 +30,8 @@ void TrumaiNetBoxApp::update() {
this->heater_.update(); this->heater_.update();
this->timer_.update(); this->timer_.update();
// Master TX scheduler (throttle to ~20ms spacing) LinBusProtocol::update();
if (this->master_mode_ && !this->master_tx_queue_.empty()) {
uint32_t now = micros();
if (now - this->last_master_send_us_ > 20000) {
auto req = this->master_tx_queue_.front(); this->master_tx_queue_.pop();
this->write_lin_master_frame_(req.pid, req.data, req.len);
this->last_master_send_us_ = now;
}
}
#ifdef USE_TIME #ifdef USE_TIME
// Update time of CP Plus automatically when // Update time of CP Plus automatically when
// - Time component configured // - Time component configured
@@ -410,40 +403,4 @@ bool TrumaiNetBoxApp::has_update_to_submit_() {
} }
} // namespace truma_inetbox } // namespace truma_inetbox
} // namespace esphome } // namespace esphome
namespace esphome { namespace truma_inetbox {
bool TrumaiNetBoxApp::master_send_diag_single(uint8_t nad, const std::vector<uint8_t> &payload) {
if (!this->master_mode_) return false;
if (payload.size() == 0 || payload.size() > 6) return false; // SID + up to 5 bytes
MasterReq r{};
r.pid = 0x3C; // DIAGNOSTIC_FRAME_MASTER
r.len = 8;
r.data[0] = nad;
r.data[1] = (uint8_t)payload.size(); // PCI: single frame, length
// Note: using low-nibble length style is sufficient; upper nibble zero.
// payload starts at data[2]
for (size_t i = 0; i < payload.size(); i++) r.data[2+i] = payload[i];
// pad remaining bytes with 0x00
for (uint8_t i = 2 + payload.size(); i < 8; i++) r.data[i] = 0x00;
this->master_tx_queue_.push(r);
return true;
}
bool TrumaiNetBoxApp::master_scan_b2(uint8_t nad, uint8_t ident_start, uint8_t ident_end) {
if (!this->master_mode_) return false;
auto ident = this->lin_identifier();
for (uint16_t id = ident_start; id <= ident_end; id++) {
std::vector<uint8_t> pl; pl.reserve(6);
pl.push_back(0xB2); // SID Read-by-Identifier
pl.push_back((uint8_t)id); // Identifier
pl.push_back(ident[0]); // 4-byte selector
pl.push_back(ident[1]);
pl.push_back(ident[2]);
pl.push_back(ident[3]);
this->master_send_diag_single(nad, pl);
}
return true;
}
} }

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <queue>
#include "LinBusProtocol.h" #include "LinBusProtocol.h"
#include "TrumaStructs.h" #include "TrumaStructs.h"
#include "TrumaiNetBoxAppAirconAuto.h" #include "TrumaiNetBoxAppAirconAuto.h"
@@ -28,9 +26,6 @@ class TrumaiNetBoxApp : public LinBusProtocol {
const std::array<u_int8_t, 4> lin_identifier() override; const std::array<u_int8_t, 4> lin_identifier() override;
void lin_heartbeat() override; void lin_heartbeat() override;
void lin_reset_device() override; void lin_reset_device() override;
// 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 );
TRUMA_DEVICE get_heater_device() const { return this->heater_device_; } TRUMA_DEVICE get_heater_device() const { return this->heater_device_; }
TRUMA_DEVICE get_aircon_device() const { return this->aircon_device_; } TRUMA_DEVICE get_aircon_device() const { return this->aircon_device_; }
@@ -80,20 +75,11 @@ class TrumaiNetBoxApp : public LinBusProtocol {
bool answer_lin_order_(const u_int8_t pid) override; bool answer_lin_order_(const u_int8_t pid) override;
bool has_update_to_submit_();
bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) override; bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) override;
const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len, const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
u_int8_t *return_len) override; u_int8_t *return_len) override;
bool has_update_to_submit_();
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;
}; };
} // namespace truma_inetbox } // namespace truma_inetbox

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Optional
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -18,16 +18,8 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_STOP, CONF_STOP,
CONF_TIME_ID, CONF_TIME_ID,
CONF_TIME) CONF_TIME,
)
CONF_MASTER_MODE = "master_mode"
CONF_WRITES_ARMED = "writes_armed"
CONF_MASTER_NAD = "master_nad"
CONF_UDP_STREAM_HOST = "udp_stream_host"
CONF_UDP_STREAM_PORT = "udp_stream_port"
CONF_STREAM_ENABLED = "stream_enabled"
CONF_STREAM_DIAG_ONLY = "stream_diag_only"
CONF_STREAM_KEEPALIVE_MS = "stream_keepalive_ms"
from esphome.components.uart import ( from esphome.components.uart import (
CONF_STOP_BITS, CONF_STOP_BITS,
CONF_DATA_BITS, CONF_DATA_BITS,
@@ -217,16 +209,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LIN_CHECKSUM, "VERSION_2"): cv.enum(CONF_SUPPORTED_LIN_CHECKSUM, upper=True), cv.Optional(CONF_LIN_CHECKSUM, "VERSION_2"): cv.enum(CONF_SUPPORTED_LIN_CHECKSUM, upper=True),
cv.Optional(CONF_CS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_CS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_FAULT_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_FAULT_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_OBSERVER_MODE, False): cv.boolean, cv.Optional(CONF_OBSERVER_MODE): cv.boolean,
cv.Optional(CONF_UDP_STREAM_HOST): cv.string,
cv.Optional(CONF_UDP_STREAM_PORT): cv.int_,
cv.Optional(CONF_STREAM_ENABLED, False): cv.boolean,
cv.Optional(CONF_STREAM_DIAG_ONLY, False): cv.boolean,
cv.Optional(CONF_STREAM_KEEPALIVE_MS, 2000): cv.positive_int,
cv.Optional(CONF_MASTER_MODE, False): cv.boolean,
cv.Optional(CONF_WRITES_ARMED, False): cv.boolean,
cv.Optional(CONF_MASTER_NAD, 0x7F): cv.int_range(min=0, max=127),
cv.Optional(CONF_ON_HEATER_MESSAGE): automation.validate_automation( cv.Optional(CONF_ON_HEATER_MESSAGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TrumaiNetBoxAppHeaterMessageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TrumaiNetBoxAppHeaterMessageTrigger),
@@ -274,22 +257,13 @@ async def to_code(config):
if CONF_OBSERVER_MODE in config: if CONF_OBSERVER_MODE in config:
cg.add(var.set_observer_mode(config[CONF_OBSERVER_MODE])) cg.add(var.set_observer_mode(config[CONF_OBSERVER_MODE]))
if CONF_UDP_STREAM_HOST in config: for conf in config.get(CONF_ON_HEATER_MESSAGE, []):
cg.add(var.set_udp_stream_host(config[CONF_UDP_STREAM_HOST])) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_UDP_STREAM_PORT in config: await automation.build_automation(
cg.add(var.set_udp_stream_port(config[CONF_UDP_STREAM_PORT])) trigger, [(StatusFrameHeaterConstPtr, "message")], conf
if CONF_STREAM_ENABLED in config: )
cg.add(var.set_stream_enabled(config[CONF_STREAM_ENABLED]))
if CONF_STREAM_DIAG_ONLY in config:
cg.add(var.set_stream_diag_only(config[CONF_STREAM_DIAG_ONLY]))
if CONF_STREAM_KEEPALIVE_MS in config:
cg.add(var.set_stream_keepalive_ms(config[CONF_STREAM_KEEPALIVE_MS]))
if CONF_MASTER_MODE in config:
cg.add(var.set_master_mode(config[CONF_MASTER_MODE]))
if CONF_WRITES_ARMED in config:
cg.add(var.set_writes_armed(config[CONF_WRITES_ARMED]))
if CONF_MASTER_NAD in config:
cg.add(var.set_master_nad(config[CONF_MASTER_NAD]))
# AUTOMATION # AUTOMATION
CONF_ENERGY_MIX = "energy_mix" CONF_ENERGY_MIX = "energy_mix"

View File

@@ -1,154 +0,0 @@
#!/usr/bin/env python3
"""
UDP receiver for LIN sniffer/master stream.
Purpose
- Capture all UDP datagrams from the ESP and write a CSV for later diffing.
- Print a live summary to the console.
- Optionally parse lines that contain textual "PID XX" + hex bytes and emit
only changes per PID (useful when the stream is verbose).
This is intentionally tolerant about payload format: it records raw hex and, if
the payload looks textual, the decoded text as well.
Examples
# Basic recording on port 5555
python udp_lin_receiver.py --port 5555 --out capture.csv
# Only show changed payloads per PID (when textual lines include "PID ..")
python udp_lin_receiver.py --port 5555 --out capture.csv --pid-diff
# Quiet mode (only CSV), capture for 5 minutes
python udp_lin_receiver.py --quiet --duration 300 --out capture.csv
"""
from __future__ import annotations
import argparse
import csv
import datetime as dt
import os
import re
import socket
import sys
import time
from typing import Dict, Optional, Tuple
def is_mostly_text(b: bytes, threshold: float = 0.9) -> bool:
if not b:
return True
printable = sum((32 <= c < 127) or c in (9, 10, 13) for c in b)
return printable / len(b) >= threshold
def to_hex(b: bytes) -> str:
return " ".join(f"{x:02X}" for x in b)
PID_LINE_RE = re.compile(r"PID\s+([0-9A-Fa-f]{2}).*?([0-9A-Fa-f]{2}(?:\s+[0-9A-Fa-f]{2})*)")
def maybe_parse_pid_and_data(text: str) -> Optional[Tuple[int, Tuple[int, ...]]]:
"""Parse lines like: 'PID 20 12 34 56 78 ...' -> (pid, (bytes...))
Returns None if no clear match.
"""
m = PID_LINE_RE.search(text)
if not m:
return None
try:
pid = int(m.group(1), 16)
data_hex = m.group(2)
data = tuple(int(x, 16) for x in data_hex.split())
return pid, data
except Exception:
return None
def now_iso() -> str:
return dt.datetime.now(dt.timezone.utc).astimezone().isoformat(timespec="milliseconds")
def run():
ap = argparse.ArgumentParser(description="UDP LIN receiver")
ap.add_argument("--host", default="0.0.0.0", help="Listen host (default: 0.0.0.0)")
ap.add_argument("--port", type=int, default=5555, help="Listen port (default: 5555)")
ap.add_argument("--out", default="udp_capture.csv", help="CSV output file path")
ap.add_argument("--append", action="store_true", help="Append to CSV if exists")
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("--buffer", type=int, default=4096, help="Receive buffer size (default: 4096)")
args = ap.parse_args()
# Prepare CSV
out_exists = os.path.exists(args.out)
csv_mode = "a" if args.append else "w"
with open(args.out, csv_mode, newline="", encoding="utf-8") as f:
w = csv.writer(f)
if not out_exists or not args.append:
w.writerow(["ts", "src_ip", "src_port", "size", "is_text", "text", "hex"]) # header
# Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((args.host, args.port))
sock.settimeout(1.0)
if not args.quiet:
print(f"Listening on {args.host}:{args.port}{args.out}")
if args.pid_diff:
print("PID diff mode: will summarize changes per PID when textual lines include 'PID ..' and hex bytes")
start = time.time()
last_by_pid: Dict[int, Tuple[int, ...]] = {}
while True:
if args.duration and (time.time() - start) >= args.duration:
if not args.quiet:
print("Duration reached; exiting.")
break
try:
data, addr = sock.recvfrom(args.buffer)
except socket.timeout:
continue
ts = now_iso()
src_ip, src_port = addr[0], addr[1]
size = len(data)
as_text = ""
is_text = False
try:
if is_mostly_text(data):
as_text = data.decode("utf-8", errors="replace").strip()
is_text = True
except Exception:
pass
hexstr = to_hex(data)
w.writerow([ts, src_ip, src_port, size, int(is_text), as_text, hexstr])
# Console output
if not args.quiet:
if args.pid_diff and is_text:
parsed = maybe_parse_pid_and_data(as_text)
if parsed:
pid, pdata = parsed
last = last_by_pid.get(pid)
if last != pdata:
last_by_pid[pid] = pdata
print(f"{ts} PID {pid:02X} len={len(pdata)} changed: { ' '.join(f'{b:02X}' for b in pdata) }")
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}")
if __name__ == "__main__":
try:
run()
except KeyboardInterrupt:
pass