add UDP command processing and sender for dynamic testing

This commit is contained in:
Hendrik Groove 2025-09-15 11:17:33 +02:00
parent 8591e7433d
commit 3384c89475
3 changed files with 187 additions and 0 deletions

View File

@ -4,6 +4,14 @@
#include "esphome/core/helpers.h"
#include "helpers.h"
#ifdef USE_ESP32
#include <sstream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace esphome {
namespace truma_inetbox {
@ -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<uint8_t> payload;
std::istringstream iss(hex_data);
std::string byte_str;
while (iss >> byte_str) {
if (byte_str.length() == 2) {
uint8_t byte_val = (uint8_t)strtoul(byte_str.c_str(), nullptr, 16);
payload.push_back(byte_val);
}
}
if (!payload.empty()) {
ESP_LOGI(TAG, "Sending diagnostic command to NAD 0x%02X with %zu bytes", nad, payload.size());
this->master_send_diag_single(nad, payload);
} else {
ESP_LOGW(TAG, "Failed to parse command payload");
}
} else {
ESP_LOGW(TAG, "Invalid command format. Use: CMD:B2 23 17 46 10 03 NAD:01");
}
}
}
}
} }

View File

@ -32,6 +32,7 @@ class TrumaiNetBoxApp : public LinBusProtocol {
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_; }
@ -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

85
tools/udp_cmd_sender.py Normal file
View File

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