7 Commits

Author SHA1 Message Date
4298e675a5 truma_inetbox: fix compile on ESP32 Arduino
- Add missing member fields for stream+master config in LinBusListener
- Expose write_lin_master_frame_ as protected
- Make LinBusLog use <cstdint> and uint8_t
- Add <queue> + declarations for lin_reset_device/has_update_to_submit in TrumaiNetBoxApp.h
- Arduino: fix UART calls and add helper prototypes in LinBusListener_esp32_arduino.cpp

Tested: esphome compile tests/test.esp32_ard.yaml (build OK).
2025-09-08 14:53:35 +02:00
Automation
891d3c59e8 fix(c++): remove stray CRLF escapes and place write_lin_master_frame_ inside namespaces 2025-09-08 12:23:59 +02:00
8db35ea7a5 fix: correct formatting of import statements in __init__.py 2025-09-08 11:06:33 +02:00
Automation
e717bf7d85 fix(py): import CONF_TIME for clock action requires_component 2025-09-08 11:02:15 +02:00
Automation
de21822c7a fix(py): correct indentation in to_code for stream options 2025-09-08 10:54:13 +02:00
Automation
d5942af8a0 fix: correct __init__.py imports and add stream+master options to schema/to_code 2025-09-08 10:32:19 +02:00
Automation
422fc3469e feat(master): add guarded LIN master TX (ESP32 Arduino), B2 scanner API, and scheduler 2025-09-07 23:09:51 +02:00
6 changed files with 158 additions and 21 deletions

View File

@@ -50,17 +50,45 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
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 +143,7 @@ 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);
uint8_t lin_msg_static_queue_storage[TRUMA_MSG_QUEUE_LENGTH * sizeof(QUEUE_LIN_MSG)];
StaticQueue_t lin_msg_static_queue_;

View File

@@ -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,28 @@ 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 esphome
#undef QUEUE_WAIT_BLOCKING
#undef ESPHOME_UART
#endif // USE_ESP32_FRAMEWORK_ARDUINO
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -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;

View File

@@ -30,8 +30,15 @@ void TrumaiNetBoxApp::update() {
this->heater_.update();
this->timer_.update();
LinBusProtocol::update();
// 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
@@ -403,4 +410,40 @@ 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;
}
} }

View File

@@ -1,5 +1,7 @@
#pragma once
#include <queue>
#include "LinBusProtocol.h"
#include "TrumaStructs.h"
#include "TrumaiNetBoxAppAirconAuto.h"
@@ -26,6 +28,9 @@ 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 );
TRUMA_DEVICE get_heater_device() const { return this->heater_device_; }
TRUMA_DEVICE get_aircon_device() const { return this->aircon_device_; }
@@ -75,11 +80,20 @@ 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;
};
} // namespace truma_inetbox

View File

@@ -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,15 @@ 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_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 +273,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"