Initial AI-assisted esphome device code for Owon B35T+ multimeter.
This commit is contained in:
@@ -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<uint8_t> 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<uint8_t> 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());
|
||||
@@ -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 <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#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<uint8_t> &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<unsigned>(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<float>(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<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(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<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(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<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(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<char>(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<uint16_t>(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;
|
||||
Reference in New Issue
Block a user