Compare commits
28 Commits
main
...
feature/ud
| Author | SHA1 | Date | |
|---|---|---|---|
| 240d5cbd4b | |||
| 3384c89475 | |||
| 8591e7433d | |||
| 4c097a576d | |||
| a0837070e2 | |||
| 958293f686 | |||
| f022db25c4 | |||
| 98ad6caab9 | |||
| aa5a85ec42 | |||
| 29235365dc | |||
| ac8d902c08 | |||
| 451c184dda | |||
| 7248f611df | |||
| 02cafc62dd | |||
| b5f02d0a47 | |||
| d0632b8457 | |||
| 382a4d3b4a | |||
| 99249dfaa9 | |||
| 9e07fb6953 | |||
| 4719d483a7 | |||
| c81ffdcb12 | |||
| 4298e675a5 | |||
|
|
891d3c59e8 | ||
| 8db35ea7a5 | |||
|
|
e717bf7d85 | ||
|
|
de21822c7a | ||
|
|
d5942af8a0 | ||
|
|
422fc3469e |
@ -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
|
||||
|
||||
@ -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,21 +49,51 @@ 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);
|
||||
|
||||
// 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
|
||||
// Return is the expected wait time till next data check is recommended.
|
||||
u_int32_t onSerialEvent();
|
||||
#endif // USE_RP2040
|
||||
|
||||
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;
|
||||
GPIOPin *cs_pin_ = nullptr;
|
||||
GPIOPin *fault_pin_ = nullptr;
|
||||
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);
|
||||
bool check_for_lin_fault_();
|
||||
virtual bool answer_lin_order_(const u_int8_t pid) = 0;
|
||||
@ -115,6 +148,11 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
|
||||
void read_lin_frame_();
|
||||
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_;
|
||||
@ -135,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_;
|
||||
@ -147,4 +197,4 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
|
||||
};
|
||||
|
||||
} // namespace truma_inetbox
|
||||
} // namespace esphome
|
||||
} // namespace esphome
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#include "LinBusListener.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "driver/uart.h"
|
||||
@ -12,9 +12,17 @@
|
||||
#endif // CUSTOM_ESPHOME_UART
|
||||
#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 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";
|
||||
|
||||
#define QUEUE_WAIT_BLOCKING (portTickType) portMAX_DELAY
|
||||
@ -39,7 +47,7 @@ void LinBusListener::setup_framework() {
|
||||
uart_intr.rx_timeout_thresh =
|
||||
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_config(uart_num, &uart_intr);
|
||||
uart_intr_config((uart_port_t) uart_num, &uart_intr);
|
||||
|
||||
hw_serial->onReceive([this]() { this->onReceive_(); }, false);
|
||||
hw_serial->onReceiveError([this](hardwareSerial_error_t val) {
|
||||
@ -77,10 +85,47 @@ 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();
|
||||
|
||||
// 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);
|
||||
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();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
} // namespace truma_inetbox
|
||||
} // namespace esphome
|
||||
|
||||
#undef QUEUE_WAIT_BLOCKING
|
||||
#undef ESPHOME_UART
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
@ -66,9 +66,9 @@ enum class QUEUE_LOG_MSG_TYPE {
|
||||
// Log messages generated during interrupt are pushed to log queue.
|
||||
struct QUEUE_LOG_MSG {
|
||||
QUEUE_LOG_MSG_TYPE type;
|
||||
u_int8_t current_PID;
|
||||
u_int8_t data[9];
|
||||
u_int8_t len;
|
||||
uint8_t current_PID;
|
||||
uint8_t data[9];
|
||||
uint8_t len;
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
bool current_data_valid;
|
||||
bool message_source_know;
|
||||
|
||||
19
components/truma_inetbox/TriggerDiscoveryButton.cpp
Normal file
19
components/truma_inetbox/TriggerDiscoveryButton.cpp
Normal 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
|
||||
21
components/truma_inetbox/TriggerDiscoveryButton.h
Normal file
21
components/truma_inetbox/TriggerDiscoveryButton.h
Normal 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
|
||||
@ -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,8 +38,37 @@ void TrumaiNetBoxApp::update() {
|
||||
this->heater_.update();
|
||||
this->timer_.update();
|
||||
|
||||
LinBusProtocol::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();
|
||||
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
|
||||
// Update time of CP Plus automatically when
|
||||
// - Time component configured
|
||||
@ -47,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
|
||||
@ -403,4 +446,158 @@ bool TrumaiNetBoxApp::has_update_to_submit_() {
|
||||
}
|
||||
|
||||
} // 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;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} }
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <queue>
|
||||
|
||||
#include "LinBusProtocol.h"
|
||||
#include "TrumaStructs.h"
|
||||
#include "TrumaiNetBoxAppAirconAuto.h"
|
||||
@ -26,6 +28,11 @@ class TrumaiNetBoxApp : public LinBusProtocol {
|
||||
const std::array<u_int8_t, 4> lin_identifier() override;
|
||||
void lin_heartbeat() 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 );
|
||||
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_; }
|
||||
@ -75,11 +82,25 @@ class TrumaiNetBoxApp : public LinBusProtocol {
|
||||
|
||||
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;
|
||||
const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
|
||||
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;
|
||||
bool master_discovery_started_ = false;
|
||||
|
||||
// UDP command receiver socket
|
||||
int udp_cmd_sock_ = -1;
|
||||
bool udp_cmd_init_attempted_ = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace truma_inetbox
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@ -18,8 +18,16 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_STOP,
|
||||
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 (
|
||||
CONF_STOP_BITS,
|
||||
CONF_DATA_BITS,
|
||||
@ -209,7 +217,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
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_FAULT_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_OBSERVER_MODE): cv.boolean,
|
||||
cv.Optional(CONF_OBSERVER_MODE, False): 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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TrumaiNetBoxAppHeaterMessageTrigger),
|
||||
@ -257,13 +274,22 @@ async def to_code(config):
|
||||
if CONF_OBSERVER_MODE in config:
|
||||
cg.add(var.set_observer_mode(config[CONF_OBSERVER_MODE]))
|
||||
|
||||
for conf in config.get(CONF_ON_HEATER_MESSAGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(StatusFrameHeaterConstPtr, "message")], conf
|
||||
)
|
||||
|
||||
|
||||
if CONF_UDP_STREAM_HOST in config:
|
||||
cg.add(var.set_udp_stream_host(config[CONF_UDP_STREAM_HOST]))
|
||||
if CONF_UDP_STREAM_PORT in config:
|
||||
cg.add(var.set_udp_stream_port(config[CONF_UDP_STREAM_PORT]))
|
||||
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
|
||||
|
||||
CONF_ENERGY_MIX = "energy_mix"
|
||||
|
||||
27
components/truma_inetbox/button.py
Normal file
27
components/truma_inetbox/button.py
Normal 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
85
tools/udp_cmd_sender.py
Normal 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()
|
||||
182
tools/udp_lin_receiver.py
Normal file
182
tools/udp_lin_receiver.py
Normal file
@ -0,0 +1,182 @@
|
||||
#!/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("--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()
|
||||
|
||||
# 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, ...]] = {}
|
||||
last_message = None
|
||||
repeat_count = 0
|
||||
last_display_time = 0
|
||||
|
||||
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:
|
||||
# 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:
|
||||
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
|
||||
print(display_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user