diff --git a/components/truma_inetbox/TrumaiNetBoxApp.cpp b/components/truma_inetbox/TrumaiNetBoxApp.cpp index d891a6d..7ac944b 100644 --- a/components/truma_inetbox/TrumaiNetBoxApp.cpp +++ b/components/truma_inetbox/TrumaiNetBoxApp.cpp @@ -4,6 +4,14 @@ #include "esphome/core/helpers.h" #include "helpers.h" +#ifdef USE_ESP32 +#include +#include +#include +#include +#include +#endif + namespace esphome { namespace truma_inetbox { @@ -49,6 +57,9 @@ void TrumaiNetBoxApp::update() { 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(); @@ -503,4 +514,90 @@ void TrumaiNetBoxApp::trigger_discovery() { 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 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"); + } + } + } +} + } } diff --git a/components/truma_inetbox/TrumaiNetBoxApp.h b/components/truma_inetbox/TrumaiNetBoxApp.h index fdd21e1..1cc4170 100644 --- a/components/truma_inetbox/TrumaiNetBoxApp.h +++ b/components/truma_inetbox/TrumaiNetBoxApp.h @@ -32,6 +32,7 @@ class TrumaiNetBoxApp : public LinBusProtocol { bool master_send_diag_single(uint8_t nad, const std::vector &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_; } @@ -96,6 +97,10 @@ class TrumaiNetBoxApp : public LinBusProtocol { 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 diff --git a/tools/udp_cmd_sender.py b/tools/udp_cmd_sender.py new file mode 100644 index 0000000..cd628d4 --- /dev/null +++ b/tools/udp_cmd_sender.py @@ -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 [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() \ No newline at end of file