Compare commits
9 Commits
main
...
4719d483a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 4719d483a7 | |||
| c81ffdcb12 | |||
| 4298e675a5 | |||
|
|
891d3c59e8 | ||
| 8db35ea7a5 | |||
|
|
e717bf7d85 | ||
|
|
de21822c7a | ||
|
|
d5942af8a0 | ||
|
|
422fc3469e |
@@ -50,17 +50,45 @@ class LinBusListener : public PollingComponent, public uart::UARTDevice {
|
|||||||
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;
|
||||||
@@ -115,6 +143,7 @@ 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);
|
||||||
|
|
||||||
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_;
|
||||||
|
|||||||
@@ -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,9 +12,17 @@
|
|||||||
#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
|
||||||
@@ -39,7 +47,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_num, &uart_intr);
|
uart_intr_config((uart_port_t) 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) {
|
||||||
@@ -77,6 +85,24 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
u_int8_t current_PID;
|
uint8_t current_PID;
|
||||||
u_int8_t data[9];
|
uint8_t data[9];
|
||||||
u_int8_t len;
|
uint8_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;
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ void TrumaiNetBoxApp::update() {
|
|||||||
this->heater_.update();
|
this->heater_.update();
|
||||||
this->timer_.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
|
#ifdef USE_TIME
|
||||||
// Update time of CP Plus automatically when
|
// Update time of CP Plus automatically when
|
||||||
// - Time component configured
|
// - Time component configured
|
||||||
@@ -404,3 +411,39 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
#include "LinBusProtocol.h"
|
#include "LinBusProtocol.h"
|
||||||
#include "TrumaStructs.h"
|
#include "TrumaStructs.h"
|
||||||
#include "TrumaiNetBoxAppAirconAuto.h"
|
#include "TrumaiNetBoxAppAirconAuto.h"
|
||||||
@@ -26,6 +28,9 @@ 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_; }
|
||||||
@@ -75,11 +80,20 @@ 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
|
||||||
|
|||||||
@@ -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,8 +18,16 @@ 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,
|
||||||
@@ -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_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): 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.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),
|
||||||
@@ -257,13 +274,22 @@ 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]))
|
||||||
|
|
||||||
for conf in config.get(CONF_ON_HEATER_MESSAGE, []):
|
if CONF_UDP_STREAM_HOST in config:
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
cg.add(var.set_udp_stream_host(config[CONF_UDP_STREAM_HOST]))
|
||||||
await automation.build_automation(
|
if CONF_UDP_STREAM_PORT in config:
|
||||||
trigger, [(StatusFrameHeaterConstPtr, "message")], conf
|
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
|
# AUTOMATION
|
||||||
|
|
||||||
CONF_ENERGY_MIX = "energy_mix"
|
CONF_ENERGY_MIX = "energy_mix"
|
||||||
|
|||||||
154
tools/udp_lin_receiver.py
Normal file
154
tools/udp_lin_receiver.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
Reference in New Issue
Block a user