Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions components/ble_elm327/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> &` 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.
Expand Down
11 changes: 10 additions & 1 deletion components/ble_elm327/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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_))
92 changes: 84 additions & 8 deletions components/ble_elm327/ble_elm327.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ float BleElm327Device::parse_float(const std::vector<uint8_t> &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];
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -214,25 +218,97 @@ bool BleElm327Component::send_command(const std::string &cmd) {
chr->write_value(reinterpret_cast<uint8_t *>(const_cast<char *>(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<const char *>(data), length);
process_response(resp);
rx_buffer_.append(reinterpret_cast<const char *>(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) {
ESP_LOGD(TAG, "<< %s", response.c_str());

if (elm_state_ != ElmState::READY) return;

const std::string &resp = response;
// Split response by carriage return / newline
std::vector<std::string> 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<unsigned char>(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<unsigned char>(c))) {
multiline_hex += c;
}
}
} else {
std::string hex_part;
for (char c : line) {
if (std::isxdigit(static_cast<unsigned char>(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<unsigned char>(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;
Expand Down
6 changes: 4 additions & 2 deletions components/ble_elm327/ble_elm327.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float(uint8_t, uint8_t, uint8_t, uint8_t)> f) { formula_ = f; }
void set_formula(std::function<float(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, const std::vector<uint8_t> &)> f) { formula_ = f; }
void add_pre_command(const std::string &cmd) { pre_commands_.push_back(cmd + "\r"); }
const std::vector<std::string> &get_pre_commands() const { return pre_commands_; }

Expand All @@ -62,7 +62,7 @@ class BleElm327Device : public PollingComponent {
std::string pid_;
std::string mode_{"01"};
std::vector<std::string> pre_commands_;
optional<std::function<float(uint8_t, uint8_t, uint8_t, uint8_t)>> formula_;
optional<std::function<float(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, const std::vector<uint8_t> &)>> formula_;
};

// ── Component ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/components/ble_elm327/test.esp32-idf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"