diff --git a/esphome/owon-b35t.yaml b/esphome/owon-b35t.yaml new file mode 100644 index 0000000..4e3722a --- /dev/null +++ b/esphome/owon-b35t.yaml @@ -0,0 +1,388 @@ +substitutions: + name: "owon-b35t" + friendly_name: "OWON B35T Multimeter" + device_description: "M5Stack Core 1 BLE client for OWON B35T/B35T+ multimeter with local graphical display" + owon_mac_address: !secret owon_b35t_mac_address + +esphome: + name: ${name} + friendly_name: ${friendly_name} + comment: ${device_description} + min_version: 2024.6.0 + includes: + - owon_b35t.h + project: + name: "custom.owon-b35t-m5stack" + version: "1.0" + +esp32: + board: m5stack-core-esp32 + framework: + type: esp-idf + advanced: + minimum_chip_revision: "3.1" + +logger: + level: INFO + +api: + encryption: + key: !secret apikey + +ota: + platform: esphome + password: !secret ota + +wifi: + ssid: "Voltage-legacy" + password: !secret voltage_legacy_psk + #use_address: ${name}.home + power_save_mode: none + fast_connect: on + min_auth_mode: WPA2 + ap: + ssid: "OWON B35T Fallback Hotspot" + password: !secret fallback_psk + +captive_portal: + +esp32_ble_tracker: + scan_parameters: + active: true + +ble_client: + - mac_address: ${owon_mac_address} + id: owon_ble_client + on_connect: + then: + - lambda: |- + owon_meter.on_connect(); + on_disconnect: + then: + - lambda: |- + owon_meter.on_disconnect(); + +spi: + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + +output: + - platform: ledc + pin: GPIO32 + id: lcd_backlight + +light: + - platform: monochromatic + output: lcd_backlight + name: "${friendly_name} Backlight" + id: backlight + restore_mode: ALWAYS_ON + +font: + - file: "fonts/Roboto-Regular.ttf" + id: meter_font + size: 15 + glyphs: + [ + " ", + "!", + "%", + "+", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "<", + ">", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "L", + "M", + "N", + "O", + "P", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "c", + "d", + "e", + "f", + "g", + "i", + "k", + "m", + "n", + "o", + "r", + "s", + "t", + "u", + "v", + "w", + "y", + "z", + "°", + "µ", + "Ω", + ] + +display: + - platform: ili9xxx + id: lcd + model: M5STACK + cs_pin: GPIO14 + dc_pin: GPIO27 + reset_pin: GPIO33 + invert_colors: false + color_palette: 8BIT + rotation: 0 + update_interval: 500ms + lambda: |- + owon_meter.render(it, id(meter_font)); + +binary_sensor: + - platform: gpio + id: button_a + pin: + number: GPIO39 + inverted: true + internal: true + on_press: + then: + - lambda: |- + owon_meter.previous_button(); + - platform: gpio + id: button_b + pin: + number: GPIO38 + inverted: true + internal: true + on_click: + - min_length: 50ms + max_length: 1500ms + then: + - logger.log: + level: INFO + format: "OWON short press: %s" + args: ["owon_meter.selected_button_name()"] + - ble_client.ble_write: + id: owon_ble_client + service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" + characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + value: !lambda |- + std::vector data = {owon_meter.selected_button, 0x01}; + return data; + - min_length: 1500ms + max_length: 5000ms + then: + - logger.log: + level: INFO + format: "OWON long press: %s" + args: ["owon_meter.selected_button_name()"] + - ble_client.ble_write: + id: owon_ble_client + service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" + characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" + 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}; + return data; + - platform: gpio + id: button_c + pin: + number: GPIO37 + inverted: true + internal: true + on_press: + then: + - lambda: |- + owon_meter.next_button(); + + - platform: template + name: "${friendly_name} Connected" + lambda: |- + return owon_meter.connected; + - platform: template + name: "${friendly_name} Overload" + lambda: |- + return owon_meter.overload; + - platform: template + name: "${friendly_name} Low Battery" + lambda: |- + return owon_meter.low_battery; + - platform: template + name: "${friendly_name} Auto Range" + lambda: |- + return owon_meter.auto_range(); + - platform: template + name: "${friendly_name} Hold" + lambda: |- + return owon_meter.hold(); + - platform: template + name: "${friendly_name} Relative" + lambda: |- + return owon_meter.relative(); + - platform: template + name: "${friendly_name} AC" + lambda: |- + return owon_meter.ac(); + - platform: template + name: "${friendly_name} DC" + lambda: |- + return owon_meter.dc(); + - platform: template + name: "${friendly_name} Continuity" + lambda: |- + return owon_meter.continuity(); + - platform: template + name: "${friendly_name} Diode" + lambda: |- + return owon_meter.diode(); + +sensor: + - platform: ble_client + type: characteristic + ble_client_id: owon_ble_client + id: owon_notify_source + internal: true + service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" + characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb" + notify: true + update_interval: never + lambda: |- + owon_meter.handle_notify(x); + return owon_meter.value(); + + - platform: wifi_signal + name: "${friendly_name} WiFi Signal" + update_interval: 60s + + - platform: template + name: "${friendly_name} Display Value" + id: owon_display_value + accuracy_decimals: 6 + update_interval: 1s + lambda: |- + return owon_meter.has_reading && !owon_meter.overload ? owon_meter.value() : NAN; + + - platform: template + name: "${friendly_name} Base Value" + id: owon_base_value + accuracy_decimals: 9 + update_interval: 1s + lambda: |- + return owon_meter.has_reading && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Voltage" + device_class: voltage + unit_of_measurement: "V" + accuracy_decimals: 6 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_VOLTAGE && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Current" + device_class: current + unit_of_measurement: "A" + accuracy_decimals: 6 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_CURRENT && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Resistance" + unit_of_measurement: "Ω" + accuracy_decimals: 3 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_RESISTANCE && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Frequency" + device_class: frequency + unit_of_measurement: "Hz" + accuracy_decimals: 3 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_FREQUENCY && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Capacitance" + unit_of_measurement: "F" + accuracy_decimals: 12 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_CAPACITANCE && !owon_meter.overload ? owon_meter.value_base() : NAN; + + - platform: template + name: "${friendly_name} Temperature" + device_class: temperature + unit_of_measurement: "°C" + accuracy_decimals: 2 + update_interval: 1s + lambda: |- + if (owon_meter.kind() == owon_b35t::Meter::KIND_TEMP_C && !owon_meter.overload) return owon_meter.value(); + if (owon_meter.kind() == owon_b35t::Meter::KIND_TEMP_F && !owon_meter.overload) return (owon_meter.value() - 32.0f) * 5.0f / 9.0f; + return NAN; + + - platform: template + name: "${friendly_name} Duty Cycle" + unit_of_measurement: "%" + accuracy_decimals: 2 + update_interval: 1s + lambda: |- + return owon_meter.kind() == owon_b35t::Meter::KIND_DUTY && !owon_meter.overload ? owon_meter.value() : NAN; + +text_sensor: + - platform: template + name: "${friendly_name} Reading" + update_interval: 1s + lambda: |- + return owon_meter.reading_text(); + - platform: template + name: "${friendly_name} Unit" + update_interval: 1s + lambda: |- + return std::string(owon_meter.scale()) + owon_meter.unit(); + - platform: template + name: "${friendly_name} Mode" + update_interval: 1s + lambda: |- + return owon_meter.mode_text(); + - platform: template + name: "${friendly_name} Meter Type" + update_interval: 1s + lambda: |- + return owon_meter.is_plus ? std::string("B35T+") : std::string("B35T"); + - platform: template + name: "${friendly_name} Selected Button" + update_interval: 1s + lambda: |- + return std::string(owon_meter.selected_button_name()); diff --git a/esphome/owon_b35t.h b/esphome/owon_b35t.h new file mode 100644 index 0000000..0f27633 --- /dev/null +++ b/esphome/owon_b35t.h @@ -0,0 +1,439 @@ +/* + * ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1. + * Parser is based on the standalone Arduino sketch by Reaper7 + * (Beerware license, Revision 42) and Dean Cording's owonb35 notes. + */ +#pragma once + +#include +#include +#include +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/components/display/display.h" + +namespace owon_b35t { + +using esphome::Color; +using esphome::display::Display; + +static const char *const TAG = "owon_b35t"; + +class Meter { + public: + static constexpr uint8_t REGPLUSMINUS = 0x00; + static constexpr uint8_t FLAGPLUS = 0b00101011; + static constexpr uint8_t FLAGMINUS = 0b00101101; + static constexpr uint8_t REGDIG1 = 0x01; + static constexpr uint8_t REGDIG2 = 0x02; + static constexpr uint8_t REGDIG3 = 0x03; + static constexpr uint8_t REGDIG4 = 0x04; + static constexpr uint8_t REGPOINT = 0x06; + static constexpr uint8_t FLAGPOINT0 = 0b00110000; + static constexpr uint8_t FLAGPOINT1 = 0b00110001; + static constexpr uint8_t FLAGPOINT2 = 0b00110010; + static constexpr uint8_t FLAGPOINT3 = 0b00110100; + static constexpr uint8_t REGMODE = 0x07; + static constexpr uint8_t FLAGMODEHOLD = 0b00000010; + static constexpr uint8_t FLAGMODEREL = 0b00000100; + static constexpr uint8_t FLAGMODEAC = 0b00001000; + static constexpr uint8_t FLAGMODEDC = 0b00010000; + static constexpr uint8_t FLAGMODEAUTO = 0b00100000; + static constexpr uint8_t REGMINMAX = 0x08; + static constexpr uint8_t FLAGMIN = 0b00010000; + static constexpr uint8_t FLAGMAX = 0b00100000; + static constexpr uint8_t REGSCALE = 0x09; + static constexpr uint8_t FLAGSCALEDUTY = 0b00000010; + static constexpr uint8_t FLAGSCALEDIODE = 0b00000100; + static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000; + static constexpr uint8_t FLAGSCALEMEGA = 0b00010000; + static constexpr uint8_t FLAGSCALEKILO = 0b00100000; + static constexpr uint8_t FLAGSCALEMILLI = 0b01000000; + static constexpr uint8_t FLAGSCALEMICRO = 0b10000000; + static constexpr uint8_t REGUNIT = 0x0a; + static constexpr uint8_t FLAGUNITFAHR = 0b00000001; + static constexpr uint8_t FLAGUNITGRAD = 0b00000010; + static constexpr uint8_t FLAGUNITNF = 0b00000100; + static constexpr uint8_t FLAGUNITHZ = 0b00001000; + static constexpr uint8_t FLAGUNITHFE = 0b00010000; + static constexpr uint8_t FLAGUNITOHM = 0b00100000; + 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}; + + bool handle_notify(const std::vector &data) { + if (data.size() > sizeof(this->raw_)) + return false; + 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) { + memset(this->value_, 0, sizeof(this->value_)); + memcpy(this->value_, data.data(), data.size()); + this->is_plus = false; + } else { + ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast(data.size())); + return false; + } + + 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->has_reading = true; + this->last_notify_ms = millis(); + return true; + } + + void on_connect() { + this->connected = true; + this->write_available = true; + } + + void on_disconnect() { + this->connected = false; + this->write_available = false; + } + + float value() const { return this->display_value; } + float value_base() const { return this->base_value; } + 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; } + bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; } + bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; } + bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; } + bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; } + bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; } + bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; } + bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; } + + const char *unit() const { + switch (this->value_[REGUNIT]) { + case FLAGUNITFAHR: return "°F"; + case FLAGUNITGRAD: return "°C"; + case FLAGUNITNF: return "nF"; + case FLAGUNITHZ: return "Hz"; + case FLAGUNITHFE: return "hFE"; + case FLAGUNITOHM: return "Ω"; + case FLAGUNITAMP: return "A"; + case FLAGUNITVOLT: return "V"; + default: return ""; + } + } + + const char *scale() const { + if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%"; + if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M"; + if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k"; + if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m"; + if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ"; + return ""; + } + + std::string mode_text() const { + std::string out; + if (this->dc()) out += "DC "; + if (this->ac()) out += "AC "; + if (this->auto_range()) out += "AUTO "; + if (this->hold()) out += "HOLD "; + if (this->relative()) out += "REL "; + if (this->min_mode()) out += "MIN "; + if (this->max_mode()) out += "MAX "; + if (this->diode()) out += "DIODE "; + if (this->continuity()) out += "CONT "; + if (!out.empty()) out.pop_back(); + return out; + } + + std::string reading_text() const { + if (!this->connected) return "Disconnected"; + if (!this->has_reading) return "Waiting for data"; + if (this->overload) return "OL " + std::string(this->scale()) + this->unit(); + char buf[48]; + snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit()); + std::string out(buf); + auto mode = this->mode_text(); + if (!mode.empty()) out += " " + mode; + return out; + } + + 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; + switch (this->value_[REGUNIT]) { + case FLAGUNITVOLT: return KIND_VOLTAGE; + case FLAGUNITAMP: return KIND_CURRENT; + case FLAGUNITOHM: return KIND_RESISTANCE; + case FLAGUNITHZ: return KIND_FREQUENCY; + case FLAGUNITNF: return KIND_CAPACITANCE; + case FLAGUNITGRAD: return KIND_TEMP_C; + case FLAGUNITFAHR: return KIND_TEMP_F; + default: return KIND_OTHER; + } + } + + 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; + if (index < 1) index = 1; + if (index > 6) index = 6; + return names[index - 1]; + } + + void previous_button() { + if (this->selected_button > 1) this->selected_button--; + } + void next_button() { + if (this->selected_button < 6) this->selected_button++; + } + + void render(Display &it, esphome::display::BaseFont *font) { + const Color bg(0, 0, 0); + const Color fg(210, 210, 210); + const Color inactive(45, 45, 45); + const Color yellow(255, 220, 0); + const Color blue(0, 80, 255); + const Color cyan(0, 255, 255); + const Color magenta(255, 0, 255); + const Color red(255, 0, 0); + const Color green(0, 220, 0); + const Color orange(255, 165, 0); + + it.fill(bg); + this->label_(it, font, 12, 8, "BAT", this->low_battery ? red : green); + this->label_(it, font, 46, 8, "BLE", this->connected ? blue : inactive); + this->label_(it, font, 86, 8, "AUTO", this->auto_range() ? fg : inactive); + this->label_(it, font, 138, 8, "MAX", this->max_mode() ? red : inactive); + this->label_(it, font, 178, 8, "MIN", this->min_mode() ? green : inactive); + this->label_(it, font, 218, 8, "HOLD", this->hold() ? blue : inactive); + this->label_(it, font, 270, 8, "REL", this->relative() ? Color(128, 128, 0) : inactive); + + this->label_(it, font, 8, 72, "DC", this->dc() ? cyan : inactive); + this->label_(it, font, 8, 96, "AC", this->ac() ? magenta : inactive); + + if (!this->connected) { + this->draw_digits_(it, "----", false, inactive); + it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect"); + } else if (!this->has_reading) { + this->draw_digits_(it, "8888", false, inactive); + it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting"); + } else if (this->overload) { + this->draw_digits_(it, " OL ", false, fg); + } else { + char d[5]; + d[0] = this->digit_char_(REGDIG1); + d[1] = this->digit_char_(REGDIG2); + d[2] = this->digit_char_(REGDIG3); + d[3] = this->digit_char_(REGDIG4); + d[4] = 0; + this->draw_digits_(it, d, this->negative(), fg); + this->draw_decimal_points_(it, fg); + } + + std::string unit_line = std::string(this->scale()) + this->unit(); + it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str()); + this->label_(it, font, 240, 168, "DIODE", this->diode() ? magenta : inactive); + this->label_(it, font, 240, 190, "BUZZ", this->continuity() ? orange : inactive); + + this->draw_bargraph_(it, this->has_reading && !this->overload ? this->digits_from_buffer_() : 0, this->has_reading && !this->overload); + + 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); + it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<"); + it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name()); + it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">"); + } + + 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}; + + 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; + if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100; + if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10; + if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0'); + return out; + } + + float calc_display_value_() const { + if (this->overload) return NAN; + uint8_t decimal = 0; + switch (this->value_[REGPOINT] & 0x07) { + case 0b001: decimal = 1; break; + case 0b010: decimal = 2; break; + case 0b100: decimal = 3; break; + default: break; + } + float v = static_cast(this->digits_from_buffer_()) / std::pow(10.0f, decimal); + return this->negative() ? -v : v; + } + + float calc_base_value_() const { + if (std::isnan(this->display_value)) return NAN; + if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f; + if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f; + if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f; + if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f; + if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f; + return this->display_value; + } + + void parse_plus_() { + memset(this->value_, 0, sizeof(this->value_)); + this->value_[5] = 0x20; + this->value_[12] = 0x0D; + this->value_[13] = 0x0A; + + 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; + + switch (decimal) { + case 0: this->value_[REGPOINT] = FLAGPOINT0; break; + case 1: this->value_[REGPOINT] = FLAGPOINT3; break; + case 2: this->value_[REGPOINT] = FLAGPOINT2; break; + case 3: this->value_[REGPOINT] = FLAGPOINT1; break; + default: break; + } + + switch (function) { + case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break; + case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break; + case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break; + case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break; + case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break; + case 5: this->value_[REGUNIT] |= FLAGUNITNF; break; + case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break; + case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break; + case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break; + case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break; + case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break; + case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break; + case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break; + default: break; + } + + switch (scale) { + case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break; + case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break; + case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break; + case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break; + default: break; + } + + 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; + if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO; + this->low_battery = (pair2 & (1 << 3)) != 0; + if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN; + if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX; + + uint16_t pair3 = static_cast(this->raw_[4]) | (static_cast(this->raw_[5]) << 8); + if (decimal < 7) { + uint16_t digits = pair3; + if (pair3 < 0x7FFF) { + this->value_[REGPLUSMINUS] = FLAGPLUS; + } else { + this->value_[REGPLUSMINUS] = FLAGMINUS; + digits = pair3 & 0x7FFF; + } + this->value_[REGDIG1] = '0' + ((digits / 1000) % 10); + this->value_[REGDIG2] = '0' + ((digits / 100) % 10); + this->value_[REGDIG3] = '0' + ((digits / 10) % 10); + this->value_[REGDIG4] = '0' + (digits % 10); + } else { + memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)); + } + } + + char digit_char_(uint8_t reg) const { + uint8_t c = this->value_[reg]; + return (c >= '0' && c <= '9') ? static_cast(c) : ' '; + } + + 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); + } + + void draw_digits_(Display &it, const char *text, bool negative, Color color) { + if (negative) it.filled_rectangle(8, 88, 24, 8, color); + int x = 42; + for (int i = 0; i < 4; i++) { + this->draw_seven_segment_(it, x + i * 55, 38, 42, 82, text[i], color); + } + } + + void draw_decimal_points_(Display &it, Color color) { + uint8_t p = this->value_[REGPOINT]; + if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(92, 116, 8, 10, color); + if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(147, 116, 8, 10, color); + if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(202, 116, 8, 10, color); + } + + void draw_segment_(Display &it, int x, int y, int w, int h, Color color) { it.filled_rectangle(x, y, w, h, color); } + + 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) { + case '0': a=b=c=d=e=f=true; break; + case '1': b=c=true; break; + case '2': a=b=d=e=g=true; break; + case '3': a=b=c=d=g=true; break; + case '4': b=c=f=g=true; break; + case '5': a=c=d=f=g=true; break; + case '6': a=c=d=e=f=g=true; break; + case '7': a=b=c=true; break; + case '8': a=b=c=d=e=f=g=true; break; + case '9': a=b=c=d=f=g=true; break; + case 'O': a=b=c=d=e=f=true; break; + case 'L': d=e=f=true; break; + case '-': g=true; break; + default: break; + } + int t = 8; + if (a) this->draw_segment_(it, x + t, y, w - 2*t, t, color); + if (b) this->draw_segment_(it, x + w - t, y + t, t, h/2 - t, color); + if (c) this->draw_segment_(it, x + w - t, y + h/2, t, h/2 - t, color); + if (d) this->draw_segment_(it, x + t, y + h - t, w - 2*t, t, color); + if (e) this->draw_segment_(it, x, y + h/2, t, h/2 - t, color); + if (f) this->draw_segment_(it, x, y + t, t, h/2 - t, color); + if (g) this->draw_segment_(it, x + t, y + h/2 - t/2, w - 2*t, t, color); + } + + void draw_bargraph_(Display &it, uint16_t digits, bool active) { + const Color fg(255, 255, 255); + const Color inactive(45, 45, 45); + uint16_t mapped = active ? static_cast(digits * 240 / 6000) : 0; + if (mapped > 240) mapped = 240; + for (uint16_t i = 0; i <= 240; i += 4) { + Color col = (active && i <= mapped) ? fg : inactive; + int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10); + it.vertical_line(40 + i, 185 - h, h, col); + } + it.horizontal_line(35, 185, 250, inactive); + } +}; + +} // namespace owon_b35t + +static owon_b35t::Meter owon_meter;