diff --git a/components/ble_elm327/README.md b/components/ble_elm327/README.md index f4b0b2f7..2481002e 100644 --- a/components/ble_elm327/README.md +++ b/components/ble_elm327/README.md @@ -590,23 +590,44 @@ The component strips the first **3 bytes** (response code + 2-byte PID echo) and ## Formula Lambda -The `formula` lambda receives up to four `uint8_t` arguments — `a`, `b`, `c`, `d` — corresponding to the first four payload bytes after header stripping. Parameters beyond the actual response length default to `0`. The lambda must return a `float`. +The `formula` lambda receives the following arguments: +- `a`, `b`, `c`, `d`, `e`, `f`: The first six payload bytes (`uint8_t`) after header/echo stripping (defaulting to `0` if the response is shorter than the index). +- `x`: A `const std::vector &` containing the full vector of all parsed payload bytes. This is particularly useful for accessing bytes beyond the 6th byte (index 5) in long responses. +The lambda must return a `float`. + +### Examples + +#### Single byte ```yaml -formula: "return a;" # single byte +formula: "return a;" ``` +#### 2-byte big-endian ÷ 4 ```yaml -formula: "return (a * 256.0f + b) / 4.0f;" # 2-byte big-endian ÷ 4 +formula: "return (a * 256.0f + b) / 4.0f;" ``` +#### 4-byte big-endian ÷ 10 ```yaml formula: |- uint32_t raw = ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | (uint32_t)d; - return raw / 10.0f; # 4-byte big-endian ÷ 10 + return raw / 10.0f; +``` + +#### Accessing 5th and 6th bytes (using e and f) +If you are querying a multiline PID and need to access the 5th and 6th payload bytes (index 4 and 5, corresponding to bits 32-47 of the parsed payload): +```yaml +formula: "return (e * 256.0f + f);" +``` + +#### Accessing arbitrary bytes beyond the 6th byte +If you need to access bytes beyond the 6th byte (e.g. index 6): +```yaml +formula: "return x[6];" ``` When no `formula` is provided the component concatenates all received payload bytes as a big-endian integer. diff --git a/components/ble_elm327/__init__.py b/components/ble_elm327/__init__.py index 10e3d0d9..785e1316 100644 --- a/components/ble_elm327/__init__.py +++ b/components/ble_elm327/__init__.py @@ -17,6 +17,7 @@ ble_elm327_ns = cg.esphome_ns.namespace("ble_elm327") +vector_uint8 = cg.std_vector.template(cg.uint8) # Component is NOT a PollingComponent — per-device polling only BleElm327Component = ble_elm327_ns.class_( "BleElm327Component", cg.Component, ble_client.BLEClientNode @@ -125,7 +126,15 @@ async def register_ble_elm327_device(var, config): if CONF_FORMULA in config: formula_ = await cg.process_lambda( config[CONF_FORMULA], - [(cg.uint8, "a"), (cg.uint8, "b"), (cg.uint8, "c"), (cg.uint8, "d")], + [ + (cg.uint8, "a"), + (cg.uint8, "b"), + (cg.uint8, "c"), + (cg.uint8, "d"), + (cg.uint8, "e"), + (cg.uint8, "f"), + (vector_uint8.operator("const").operator("ref"), "x"), + ], return_type=cg.float_, ) cg.add(var.set_formula(formula_)) diff --git a/components/ble_elm327/ble_elm327.cpp b/components/ble_elm327/ble_elm327.cpp index bb72c254..26d44839 100644 --- a/components/ble_elm327/ble_elm327.cpp +++ b/components/ble_elm327/ble_elm327.cpp @@ -50,7 +50,9 @@ float BleElm327Device::parse_float(const std::vector &data) { uint8_t b = data.size() > 1 ? data[1] : 0; uint8_t c = data.size() > 2 ? data[2] : 0; uint8_t d = data.size() > 3 ? data[3] : 0; - return (*formula_)(a, b, c, d); + uint8_t e = data.size() > 4 ? data[4] : 0; + uint8_t f = data.size() > 5 ? data[5] : 0; + return (*formula_)(a, b, c, d, e, f, data); } float val = 0; for (size_t i = 0; i < data.size(); i++) val = val * 256.0f + data[i]; @@ -157,6 +159,8 @@ void BleElm327Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gat while (!tx_queue_.empty()) tx_queue_.pop(); for (auto *d : devices_) d->on_dequeue(); current_pre_commands_.clear(); + rx_buffer_.clear(); + last_sent_command_.clear(); ESP_LOGW(TAG, "Disconnected from ELM327"); break; @@ -214,12 +218,18 @@ bool BleElm327Component::send_command(const std::string &cmd) { chr->write_value(reinterpret_cast(const_cast(cmd.data())), cmd.size(), ESP_GATT_WRITE_TYPE_NO_RSP); ESP_LOGD(TAG, ">> %s", cmd.c_str()); + last_sent_command_ = normalize_command(cmd); return true; } void BleElm327Component::on_notify(const uint8_t *data, uint16_t length) { - std::string resp(reinterpret_cast(data), length); - process_response(resp); + rx_buffer_.append(reinterpret_cast(data), length); + size_t pos; + while ((pos = rx_buffer_.find('>')) != std::string::npos) { + std::string response = rx_buffer_.substr(0, pos); + rx_buffer_.erase(0, pos + 1); + process_response(response); + } } void BleElm327Component::process_response(const std::string &response) { @@ -227,12 +237,78 @@ void BleElm327Component::process_response(const std::string &response) { if (elm_state_ != ElmState::READY) return; - const std::string &resp = response; + // Split response by carriage return / newline + std::vector lines; + size_t start = 0; + while (start < response.size()) { + size_t end = response.find_first_of("\r\n", start); + if (end == std::string::npos) { + lines.push_back(response.substr(start)); + break; + } + if (end > start) { + lines.push_back(response.substr(start, end - start)); + } + start = end + 1; + } + + std::string multiline_hex; + std::string singleline_hex; + + for (const auto &raw_line : lines) { + // Trim leading/trailing whitespace + std::string line = raw_line; + line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + line.erase(std::find_if(line.rbegin(), line.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), line.end()); + + if (line.empty()) continue; + + // Check if it's the command echo + if (normalize_command(line) == last_sent_command_) { + continue; + } + + // Check if it's a multiline frame line (e.g. "0: 62 E0 02...") + bool is_multiline_frame = false; + size_t colon_pos = line.find(':'); + if (colon_pos != std::string::npos && colon_pos > 0) { + bool all_digits = true; + for (size_t i = 0; i < colon_pos; i++) { + if (!std::isdigit(static_cast(line[i]))) { + all_digits = false; + break; + } + } + if (all_digits) { + is_multiline_frame = true; + } + } + + if (is_multiline_frame) { + std::string hex_part = line.substr(colon_pos + 1); + for (char c : hex_part) { + if (std::isxdigit(static_cast(c))) { + multiline_hex += c; + } + } + } else { + std::string hex_part; + for (char c : line) { + if (std::isxdigit(static_cast(c))) { + hex_part += c; + } + } + if (hex_part.size() > 3) { + singleline_hex += hex_part; + } + } + } - // Strip whitespace, CR, LF, '>' — works with both ATS0 (compact) and default (spaced) format - std::string hex; - for (char c : resp) - if (isxdigit(static_cast(c))) hex += c; + std::string hex = multiline_hex.empty() ? singleline_hex : multiline_hex; // Must have at least 4 hex chars (1-byte response code + 1-byte PID/data) if (hex.size() < 4) return; diff --git a/components/ble_elm327/ble_elm327.h b/components/ble_elm327/ble_elm327.h index f5db7b5c..246157af 100644 --- a/components/ble_elm327/ble_elm327.h +++ b/components/ble_elm327/ble_elm327.h @@ -39,7 +39,7 @@ class BleElm327Device : public PollingComponent { void set_pid(const std::string &pid) { pid_ = pid; } void set_mode(const std::string &mode) { mode_ = mode; } - void set_formula(std::function f) { formula_ = f; } + void set_formula(std::function &)> f) { formula_ = f; } void add_pre_command(const std::string &cmd) { pre_commands_.push_back(cmd + "\r"); } const std::vector &get_pre_commands() const { return pre_commands_; } @@ -62,7 +62,7 @@ class BleElm327Device : public PollingComponent { std::string pid_; std::string mode_{"01"}; std::vector pre_commands_; - optional> formula_; + optional &)>> formula_; }; // ── Component ───────────────────────────────────────────────────────────────── @@ -134,6 +134,8 @@ class BleElm327Component : public Component, public ble_client::BLEClientNode { uint32_t tx_delay_ms_{50}; uint32_t last_tx_time_{0}; + std::string rx_buffer_; + std::string last_sent_command_; }; } // namespace ble_elm327 diff --git a/tests/components/ble_elm327/test.esp32-idf.yaml b/tests/components/ble_elm327/test.esp32-idf.yaml index fd6dcc9b..635afd56 100644 --- a/tests/components/ble_elm327/test.esp32-idf.yaml +++ b/tests/components/ble_elm327/test.esp32-idf.yaml @@ -37,3 +37,11 @@ sensor: update_interval: 1s formula: "return a;" unit_of_measurement: "km/h" + + - platform: ble_elm327 + name: "Multiline Test Sensor" + pid: "E002" + mode: "22" + update_interval: 5s + formula: "return (e * 256.0f + f + x[6]);" + unit_of_measurement: "V"