From b22c86e95bc3bca5fa4e766894c89174c49e61bb Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 23 Jun 2026 10:06:08 +0200 Subject: [PATCH] Document Core2 BLE proxy power and UI logic --- esphome/lab-ble-proxy.h | 97 +++++++++++++++++++++++++++++++------- esphome/lab-ble-proxy.yaml | 62 +++++++++++++++++++++++- 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/esphome/lab-ble-proxy.h b/esphome/lab-ble-proxy.h index 1486cd6..4465429 100644 --- a/esphome/lab-ble-proxy.h +++ b/esphome/lab-ble-proxy.h @@ -25,12 +25,16 @@ namespace owon_b35t { using esphome::Color; using esphome::display::Display; +// Log tags used by ESPHome for BLE meter parsing and Core2 power-management messages. static const char *const TAG = "owon_b35t"; static const char *const POWER_TAG = "core2_power"; + +// The M5Stack Core2 routes LCD/backlight power through an AXP192 PMIC on I2C. static constexpr uint8_t AXP192_ADDR = 0x34; static esphome::i2c::I2CDevice axp192; -static bool axp192_ready = false; +static bool axp192_ready = false; // Guard PMIC accesses until the I2C bus/address has been configured. +// Write one byte to an AXP192 register and log failures instead of silently losing power changes. static bool axp_write(uint8_t reg, uint8_t value) { if (!axp192_ready) return false; bool ok = axp192.write_byte(reg, value); @@ -38,6 +42,7 @@ static bool axp_write(uint8_t reg, uint8_t value) { return ok; } +// Read one byte from an AXP192 register; callers use this before read/modify/write updates. static bool axp_read(uint8_t reg, uint8_t *value) { if (!axp192_ready) return false; bool ok = axp192.read_byte(reg, value); @@ -45,6 +50,7 @@ static bool axp_read(uint8_t reg, uint8_t *value) { return ok; } +// Clear and set selected bits in an AXP192 register while preserving all other bits. static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) { uint8_t value = 0; if (!axp_read(reg, &value)) return; @@ -52,24 +58,28 @@ static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) { axp_write(reg, value); } +// Convert a desired DCDC output voltage to the 25 mV-step register encoding used by AXP192. static uint8_t axp_dc_voltage_data(uint16_t millivolts) { if (millivolts < 700) millivolts = 700; if (millivolts > 3500) millivolts = 3500; return static_cast((millivolts - 700) / 25) & 0x7F; } +// Convert a desired LDO voltage to the 100 mV-step register encoding used by AXP192. static uint8_t axp_ldo_voltage_data(uint16_t millivolts) { if (millivolts < 1800) millivolts = 1800; if (millivolts > 3300) millivolts = 3300; return static_cast((millivolts - 1800) / 100) & 0x0F; } +// Set the LCD backlight rail (DCDC3) voltage; brightness is controlled by this rail voltage. static void core2_axp192_set_lcd_voltage(uint16_t millivolts) { uint8_t value = 0; axp_read(0x27, &value); axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight } +// Map a 0.0-1.0 brightness value to the Core2 LCD backlight voltage and enable/disable DCDC3. static void core2_axp192_set_backlight(float brightness) { if (brightness <= 0.0f) { axp_update(0x12, 0x02, 0x00); // DCDC3 off @@ -81,6 +91,7 @@ static void core2_axp192_set_backlight(float brightness) { axp_update(0x12, 0x00, 0x02); // DCDC3 on } +// Initialize the Core2 PMIC rails needed by ESP32, LCD logic, LCD backlight, and the display reset line. static void core2_axp192_init(esphome::i2c::I2CBus *bus) { axp192.set_i2c_bus(bus); axp192.set_i2c_address(AXP192_ADDR); @@ -113,6 +124,7 @@ static void core2_axp192_init(esphome::i2c::I2CBus *bus) { core2_axp192_set_backlight(1.0f); } +// 16x16 monochrome status icons drawn manually into the ESPHome display framebuffer. static const uint8_t ACCU_BMP[32] = { 0b00000000, 0b00000000, 0b00000000, 0b00000000, @@ -229,6 +241,8 @@ static const uint8_t BUZZ_BMP[32] = { class Meter { public: + // Offsets and bit masks for the 14-byte "classic" OWON frame layout. B35T+ frames are + // normalized into this same buffer so the rest of the code can use one representation. static constexpr uint8_t REGPLUSMINUS = 0x00; static constexpr uint8_t FLAGPLUS = 0b00101011; static constexpr uint8_t FLAGMINUS = 0b00101101; @@ -268,24 +282,29 @@ class Meter { static constexpr uint8_t FLAGUNITAMP = 0b01000000; static constexpr uint8_t FLAGUNITVOLT = 0b10000000; - bool connected{false}; - bool write_available{false}; - bool is_plus{false}; - bool low_battery{false}; - bool overload{false}; - bool has_reading{false}; - uint8_t selected_button{1}; - uint32_t last_notify_ms{0}; + // Connection and decoded meter state consumed by ESPHome sensors and the local display. + bool connected{false}; // BLE link to the OWON meter is currently established. + bool write_available{false}; // The writable BLE characteristic is ready for button commands. + bool is_plus{false}; // True when the last notification used the compact B35T+ frame format. + bool low_battery{false}; // Battery indicator decoded from the meter status bits. + bool overload{false}; // True when the meter reports OL instead of numeric digits. + bool has_reading{false}; // At least one valid BLE notification has been decoded. + uint8_t selected_button{1}; // UI-selected OWON remote button command (1..6). + uint32_t last_notify_ms{0}; // Timestamp of the last accepted meter notification. + // Decode a BLE notification from either supported OWON protocol variant and refresh cached values. bool handle_notify(const std::vector &data) { if (data.size() > sizeof(this->raw_)) return false; + + // B35T+ sends a compact 6-byte binary frame; convert it into the classic 14-byte layout. if (data.size() == 6 && data[1] >= 0xF0) { memset(this->raw_, 0, sizeof(this->raw_)); memcpy(this->raw_, data.data(), data.size()); this->is_plus = true; this->parse_plus_(); } else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) { + // Classic B35T sends an ASCII-like 14-byte frame terminated by CR/LF; it can be used directly. memset(this->value_, 0, sizeof(this->value_)); memcpy(this->value_, data.data(), data.size()); this->is_plus = false; @@ -294,26 +313,32 @@ class Meter { return false; } + // Cache derived states so display drawing and Home Assistant sensors do not have to re-parse frames. this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0; - this->display_value = this->calc_display_value_(); - this->base_value = this->calc_base_value_(); + this->display_value = this->calc_display_value_(); // Numeric value exactly as shown on the meter. + this->base_value = this->calc_base_value_(); // Same reading converted to base SI scale. this->has_reading = true; this->last_notify_ms = millis(); return true; } + // Called by the ESPHome BLE client once the meter connection and write characteristic are usable. void on_connect() { this->connected = true; this->write_available = true; } + // Reset connection flags when BLE disconnects; the last reading is kept for display/sensor continuity. void on_disconnect() { this->connected = false; this->write_available = false; } + // Numeric accessors used by ESPHome lambdas when publishing sensor state to Home Assistant. float value() const { return this->display_value; } float value_base() const { return this->base_value; } + + // Boolean flag helpers keep the bit-mask details out of UI and sensor publishing code. bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; } bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; } bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; } @@ -325,6 +350,7 @@ class Meter { bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; } bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; } + // Translate decoded unit bits into the text suffix shown on the display and exposed via sensors. const char *unit() const { switch (this->value_[REGUNIT]) { case FLAGUNITFAHR: return "°F"; @@ -339,6 +365,7 @@ class Meter { } } + // Translate decoded scale bits into SI prefixes or duty-cycle percent. const char *scale() const { if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%"; if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M"; @@ -348,6 +375,7 @@ class Meter { return ""; } + // Build a compact human-readable list of active meter modes for Home Assistant text sensors. std::string mode_text() const { std::string out; if (this->dc()) out += "DC "; @@ -363,6 +391,7 @@ class Meter { return out; } + // Format the current reading as a single string, including disconnected/waiting/overload states. std::string reading_text() const { if (!this->connected) return "Disconnected"; if (!this->has_reading) return "Waiting for data"; @@ -372,6 +401,7 @@ class Meter { return std::string(buf); } + // Classify the reading so ESPHome can publish it to the correct measurement-specific sensor. enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY }; Kind kind() const { if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY; @@ -387,6 +417,7 @@ class Meter { } } + // UI label for the currently selected remote-control command sent back to the OWON meter. const char *selected_button_name() const { static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"}; uint8_t index = this->selected_button; @@ -395,6 +426,7 @@ class Meter { return names[index - 1]; } + // Move the selected remote button left/right; bounds match the six commands in selected_button_name(). void previous_button() { if (this->selected_button > 1) this->selected_button--; } @@ -402,6 +434,7 @@ class Meter { if (this->selected_button < 6) this->selected_button++; } + // Draw either the OWON multimeter page or the Atorch DL24 load page on the M5Stack display. void render(esphome::display::Display &it, esphome::display::BaseFont *font, esphome::display::BaseFont *value_font, int display_page = 0, bool atorch_connected = false, @@ -422,7 +455,9 @@ class Meter { if (display_page == 0) { // --- PAGE 1: OWON Multimeter --- it.fill(bg); - bool status_active = this->connected && this->has_reading; + bool status_active = this->connected && this->has_reading; // Only highlight decoded flags after data arrives. + + // Top status row mirrors the physical meter annunciators: battery, BLE, range, min/max, hold, rel, diode, buzzer. this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive); this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive); this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive); @@ -436,6 +471,7 @@ class Meter { this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive); this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive); + // Main seven-segment area distinguishes connection, waiting-for-first-frame, overload, and normal readings. if (!this->connected) { this->draw_digits_(it, "----", false, inactive); it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect"); @@ -462,6 +498,8 @@ class Meter { bool bargraph_active = status_active && !this->overload; this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active); + + // Bottom soft-button row shows the currently selected OWON command and whether BLE writes are available. it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive); it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive); it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive); @@ -488,6 +526,7 @@ class Meter { const Color temp_color = atorch_connected ? magenta : dim_accent; const Color header_color = atorch_connected ? cyan : dim_value; + // Prepare formatted metric strings before drawing so missing/NaN Atorch values display consistently. char voltage_text[24]; char current_text[24]; char power_text[24]; @@ -508,6 +547,7 @@ class Meter { } it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD"); + // Four primary Atorch measurements are shown as dashboard cards; accumulated counters fit in the footer. this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color); this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color); this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color); @@ -526,12 +566,13 @@ class Meter { } private: - uint8_t raw_[14]{}; - uint8_t value_[14]{}; - float display_value{NAN}; - float base_value{NAN}; - static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F}; + uint8_t raw_[14]{}; // Last raw BLE payload, padded to the maximum supported OWON frame length. + uint8_t value_[14]{}; // Normalized classic-frame view used by all accessors and display rendering. + float display_value{NAN}; // Meter reading with its displayed prefix still applied, e.g. 12.3 kΩ -> 12.3. + float base_value{NAN}; // Meter reading converted to base units, e.g. 12.3 kΩ -> 12300 Ω. + static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F}; // "+?0:?" OL marker. + // Reconstruct the four visible seven-segment digits as an integer for value calculations/bargraph scaling. uint16_t digits_from_buffer_() const { uint16_t out = 0; if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000; @@ -541,10 +582,12 @@ class Meter { return out; } + // Convert the normalized frame into the floating-point number shown by the meter display. float calc_display_value_() const { if (this->overload) return NAN; if (this->is_plus) { + // B35T+ encodes decimal placement and sign/magnitude as packed little-endian words. uint16_t pair1 = static_cast(this->raw_[0]) | (static_cast(this->raw_[1]) << 8); uint8_t decimal = pair1 & 0x07; if (decimal >= 7) return NAN; @@ -556,6 +599,7 @@ class Meter { return negative ? -v : v; } + // Classic frames carry decimal placement as one of three point flags in the normalized buffer. uint8_t decimal = 0; switch (this->value_[REGPOINT] & 0x07) { case 0b001: decimal = 1; break; @@ -567,6 +611,7 @@ class Meter { return this->negative() ? -v : v; } + // Apply the decoded SI prefix so Home Assistant receives comparable base-unit sensor values. float calc_base_value_() const { if (std::isnan(this->display_value)) return NAN; if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f; @@ -577,17 +622,20 @@ class Meter { return this->display_value; } + // Decode the compact B35T+ payload and synthesize the 14-byte classic representation. void parse_plus_() { memset(this->value_, 0, sizeof(this->value_)); this->value_[5] = 0x20; this->value_[12] = 0x0D; this->value_[13] = 0x0A; + // First word: measurement function, SI scale, and decimal-point position. uint16_t pair1 = static_cast(this->raw_[0]) | (static_cast(this->raw_[1]) << 8); uint8_t function = (pair1 >> 6) & 0x0F; uint8_t scale = (pair1 >> 3) & 0x07; uint8_t decimal = pair1 & 0x07; + // Translate B35T+ decimal count to the classic point-flag convention used by draw_decimal_points_(). switch (decimal) { case 0: this->value_[REGPOINT] = FLAGPOINT0; break; case 1: this->value_[REGPOINT] = FLAGPOINT3; break; @@ -596,6 +644,7 @@ class Meter { default: break; } + // Translate the function selector into classic unit and AC/DC/mode flags. switch (function) { case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break; case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break; @@ -613,6 +662,7 @@ class Meter { default: break; } + // Translate the B35T+ scale selector into SI-prefix flags. switch (scale) { case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break; case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break; @@ -621,6 +671,7 @@ class Meter { default: break; } + // Second word: status flags such as hold, relative, auto-range, battery, and min/max. uint16_t pair2 = static_cast(this->raw_[2]) | (static_cast(this->raw_[3]) << 8); if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD; if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL; @@ -629,6 +680,7 @@ class Meter { if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN; if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX; + // Third word: signed 4-digit display value, with bit 15 indicating a negative reading. uint16_t pair3 = static_cast(this->raw_[4]) | (static_cast(this->raw_[5]) << 8); if (decimal < 7) { uint16_t digits = pair3; @@ -643,15 +695,18 @@ class Meter { this->value_[REGDIG3] = '0' + ((digits / 10) % 10); this->value_[REGDIG4] = '0' + (digits % 10); } else { + // Decimal values 7 and above are used by this protocol to indicate overload/OL. memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)); } } + // Return a printable digit for a display register, using a blank for non-digit status bytes. char digit_char_(uint8_t reg) const { uint8_t c = this->value_[reg]; return (c >= '0' && c <= '9') ? static_cast(c) : ' '; } + // Format Atorch metrics, using "--" for missing values while keeping units visible. void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const { if (!std::isfinite(value)) { snprintf(buffer, size, "-- %s", unit); @@ -662,6 +717,7 @@ class Meter { snprintf(buffer, size, format, value, unit); } + // Draw one Atorch dashboard tile with a colored accent, label, and centered numeric value. void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font, int x, int y, int w, int h, const char *title, const char *value, Color accent, Color fill, Color border, Color value_color) { @@ -678,10 +734,12 @@ class Meter { it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent); } + // Small wrapper for consistent top-left aligned labels. void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) { it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text); } + // Draw the large four-digit OWON readout, including a separate minus sign when needed. void draw_digits_(Display &it, const char *text, bool negative, Color color) { if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color); constexpr int digit_x = 40; @@ -694,6 +752,7 @@ class Meter { } } + // Draw decimal points between the large seven-segment digits according to the normalized frame flags. void draw_decimal_points_(Display &it, Color color) { uint8_t p = this->value_[REGPOINT]; if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color); @@ -701,6 +760,7 @@ class Meter { if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color); } + // Draw one beveled horizontal or vertical segment used by the custom seven-segment font. void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) { if (horizontal) { int cap = h / 2; @@ -715,6 +775,7 @@ class Meter { } } + // Plot a packed 1-bit bitmap icon, where each set bit becomes one colored display pixel. void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) { for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { @@ -726,6 +787,7 @@ class Meter { } } + // Map a digit/OL/minus character to seven segment states and draw the active segments. void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) { bool a=false,b=false,c=false,d=false,e=false,f=false,g=false; switch (ch) { @@ -766,6 +828,7 @@ class Meter { if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color); } + // Draw the OWON-style bottom bargraph by scaling raw 0..6000 display digits to a 240 px ruler. void draw_bargraph_(Display &it, uint16_t digits, bool active) { const Color fg(255, 255, 255); const Color inactive(80, 80, 80); diff --git a/esphome/lab-ble-proxy.yaml b/esphome/lab-ble-proxy.yaml index c0a4f80..3313084 100644 --- a/esphome/lab-ble-proxy.yaml +++ b/esphome/lab-ble-proxy.yaml @@ -29,6 +29,9 @@ esphome: on_boot: priority: 850 then: + # The Core2 LCD/backlight rails are controlled by the AXP192 PMIC. ESPHome's + # stock M5CORE2 display setup does not initialize those rails here, so the + # custom header performs the M5Stack-specific power sequencing once I2C exists. - lambda: |- owon_b35t::core2_axp192_init(id(core2_i2c)); project: @@ -77,10 +80,14 @@ wifi: captive_portal: globals: + # 0 = OWON meter page, 1 = Atorch DL24 page. This is intentionally volatile so + # every boot starts with the multimeter view until one device connects. - id: display_page type: int initial_value: "0" restore_value: no + # The Atorch external component does not expose a simple connected flag that the + # display lambda can query, so BLE callbacks maintain this state explicitly. - id: atorch_connected type: bool initial_value: "false" @@ -100,15 +107,19 @@ script: - delay: 2min - if: condition: + # Only dim when both BLE devices are disconnected; active lab instruments + # keep the display fully lit so readings remain visible at a glance. lambda: |- return !owon_meter.connected && !id(atorch_connected); then: - light.turn_on: id: backlight - brightness: 50% + brightness: 70% - delay: 3min - if: condition: + # Re-check before turning the backlight off, because a connection may + # have been established during the preceding delay. lambda: |- return !owon_meter.connected && !id(atorch_connected); then: @@ -117,6 +128,8 @@ script: interval: - interval: 10s then: + # Periodic memory telemetry is useful on the Core2 because BLE, PSRAM, the + # MIPI framebuffer, and external components can fragment different heap pools. - lambda: |- ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u", static_cast(esp_get_free_heap_size()), @@ -139,12 +152,16 @@ ble_client: on_connect: then: - script.execute: wake_backlight + # Notify the custom parser/display object and switch to the meter page when + # the OWON connects, so the on-device screen follows the active instrument. - lambda: |- owon_meter.on_connect(); id(display_page) = 0; id(lcd).update(); on_disconnect: then: + # Keep the UI useful after a disconnect: if the Atorch is still online, + # automatically fall back from the OWON page to the Atorch page. - lambda: |- owon_meter.on_disconnect(); if (id(display_page) == 0 && id(atorch_connected)) { @@ -153,6 +170,8 @@ ble_client: id(lcd).update(); - if: condition: + # Start the idle timer only after both BLE clients are gone; otherwise + # the remaining connected instrument still deserves an active display. lambda: |- return !owon_meter.connected && !id(atorch_connected); then: @@ -163,12 +182,16 @@ ble_client: on_connect: then: - script.execute: wake_backlight + # The Atorch component handles measurements, while this explicit flag/page + # switch keeps the shared display state synchronized with the BLE client. - lambda: |- id(atorch_connected) = true; id(display_page) = 1; id(lcd).update(); on_disconnect: then: + # If the load disconnects while the meter is still connected, move the + # physical display back to the meter instead of leaving a stale load page. - lambda: |- id(atorch_connected) = false; if (id(display_page) == 1 && owon_meter.connected) { @@ -177,6 +200,8 @@ ble_client: id(lcd).update(); - if: condition: + # Same idle rule as OWON disconnect: only dim/off when neither BLE + # target is connected anymore. lambda: |- return !owon_meter.connected && !id(atorch_connected); then: @@ -203,6 +228,8 @@ output: type: float id: lcd_backlight write_action: + # Bridge ESPHome's generic monochromatic light level to the Core2-specific + # AXP192 backlight voltage helper implemented in lab-ble-proxy.h. - lambda: |- owon_b35t::core2_axp192_set_backlight(state); @@ -217,6 +244,8 @@ font: - file: "fonts/Roboto-Regular.ttf" id: meter_font size: 15 + # Glyphs are whitelisted to save flash/RAM; include symbols used by units, + # status labels, soft-button names, and Atorch metric text. glyphs: [ " ", @@ -293,6 +322,7 @@ font: - file: "fonts/Roboto-Medium.ttf" id: atorch_value_font size: 22 + # Larger Atorch value font only needs numeric characters and measurement units. glyphs: [ " ", @@ -322,6 +352,8 @@ display: id: lcd model: M5CORE2 update_interval: 500ms + # Rendering is delegated to the custom C++ helper because ESPHome display YAML + # primitives would be unwieldy for the OWON seven-segment clone and Atorch dashboard. lambda: |- owon_meter.render( it, @@ -346,6 +378,8 @@ touchscreen: - script.execute: wake_backlight - if: condition: + # Touching an idle/disconnected unit wakes the display briefly, then + # restarts the idle timer if no BLE target is available. lambda: |- return !owon_meter.connected && !id(atorch_connected); then: @@ -361,6 +395,7 @@ binary_sensor: y_max: 195 on_press: then: + # The large middle touch zone toggles between the two custom render pages. - lambda: |- id(display_page) = 1 - id(display_page); id(lcd).update(); @@ -374,6 +409,7 @@ binary_sensor: internal: true on_press: then: + # Left soft button changes which OWON remote command the center button will send. - lambda: |- owon_meter.previous_button(); - platform: touchscreen @@ -396,6 +432,8 @@ binary_sensor: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # OWON remote-control payload: first byte selects the meter button, + # second byte selects the normal/short-press action variant. value: !lambda |- std::vector data = {owon_meter.selected_button, 0x01}; return data; @@ -410,6 +448,8 @@ binary_sensor: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # Long-press encoding is not uniform: SELECT and HZ/DUTY still use + # 0x01, while RANGE/HOLD/REL/MAX-MIN use 0x00 for their alternate action. value: !lambda |- uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00; std::vector data = {owon_meter.selected_button, press_type}; @@ -424,12 +464,14 @@ binary_sensor: internal: true on_press: then: + # Right soft button advances to the next OWON command label. - lambda: |- owon_meter.next_button(); - platform: template name: "${friendly_name} OWON Connected" icon: "mdi:bluetooth-connect" + # Template binary sensors expose state maintained by the custom OWON parser object. lambda: |- return owon_meter.connected; - platform: template @@ -450,6 +492,8 @@ binary_sensor: return id(atorch_connected); sensor: + # This hidden characteristic sensor is the bridge from OWON BLE notifications into + # custom C++ parsing. The returned float becomes the source numeric meter reading. - platform: ble_client type: characteristic ble_client_id: owon_ble_client @@ -488,6 +532,8 @@ sensor: name: "${friendly_name} Atorch Dim Backlight" id: atorch_dim_backlight + # The DL24 component exposes power, but this config computes Wh locally so the + # accumulated energy can be restored and reset alongside Atorch's own counters. - platform: integration name: "${friendly_name} Atorch Energy" id: atorch_energy_calculated @@ -506,18 +552,21 @@ text_sensor: name: "${friendly_name} OWON Reading" update_interval: 1s icon: "mdi:gauge" + # Human-readable mirror of the meter LCD, including waiting/disconnected/OL states. lambda: |- return owon_meter.reading_text(); - platform: template name: "${friendly_name} OWON Unit" update_interval: 1s icon: "mdi:ruler" + # Combines SI prefix and unit into one HA text entity, e.g. mV, kΩ, µA, %. lambda: |- return std::string(owon_meter.scale()) + owon_meter.unit(); - platform: template name: "${friendly_name} OWON Mode" icon: "mdi:knob" update_interval: 1s + # Exposes decoded annunciators such as AC/DC, AUTO, HOLD, REL, MIN/MAX. lambda: |- return owon_meter.mode_text(); @@ -535,6 +584,7 @@ button: reset_all: name: "${friendly_name} Atorch Reset All" on_press: + # Keep the locally integrated Wh counter in sync with the DL24 reset-all command. - sensor.integration.reset: atorch_energy_calculated usb_plus: name: "${friendly_name} Atorch Plus" @@ -544,6 +594,8 @@ button: name: "${friendly_name} Atorch Setup" enter: name: "${friendly_name} Atorch Enter" + # Home Assistant button entities for direct OWON remote commands. The payloads + # are the same two-byte command format used by the touchscreen soft buttons. - platform: template name: "OWON SELECT" id: owon_btn_select @@ -552,6 +604,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # SELECT normal press. value: !lambda "return std::vector({1, 0x01});" - platform: template @@ -562,6 +615,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # RANGE normal press. value: !lambda "return std::vector({2, 0x01});" - platform: template @@ -572,6 +626,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # HOLD is the short/normal action on the shared HLD/LIG physical key. value: !lambda "return std::vector({3, 0x01});" - platform: template @@ -582,6 +637,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # LIGHT is the alternate/long action on the same HLD/LIG key. value: !lambda "return std::vector({3, 0x00});" - platform: template @@ -592,6 +648,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # RELATIVE is the short/normal action on the shared REL/BT key. value: !lambda "return std::vector({4, 0x01});" - platform: template @@ -602,6 +659,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # BT is the alternate/long action on the same REL/BT key. value: !lambda "return std::vector({4, 0x00});" - platform: template @@ -612,6 +670,7 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # HZ/DUTY command uses the normal action variant. value: !lambda "return std::vector({5, 0x01});" - platform: template @@ -622,4 +681,5 @@ button: id: owon_ble_client service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + # MAX/MIN command uses the normal action variant. value: !lambda "return std::vector({6, 0x01});"