Merge branch 'beta' into bump-2022.11.0

This commit is contained in:
Jesse Hills 2022-11-17 07:17:41 +13:00
commit d1cdfd3b72
No known key found for this signature in database
GPG Key ID: BEAAE804EFD8E83A
154 changed files with 7607 additions and 1209 deletions

View File

@ -1,6 +1,6 @@
{
"name": "ESPHome Dev",
"image": "esphome/esphome-lint:dev",
"image": "ghcr.io/esphome/esphome-lint:dev",
"postCreateCommand": [
"script/devcontainer-post-create"
],

View File

@ -1,6 +1,6 @@
# What does this implement/fix?
Quick description and explanation of changes
<!-- Quick description and explanation of changes -->
## Types of changes
@ -18,6 +18,7 @@ Quick description and explanation of changes
- [ ] ESP32
- [ ] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
## Example entry for `config.yaml`:
<!--

View File

@ -48,6 +48,10 @@ jobs:
file: tests/test5.yaml
name: Test tests/test5.yaml
pio_cache_key: test5
- id: test
file: tests/test6.yaml
name: Test tests/test6.yaml
pio_cache_key: test6
- id: pytest
name: Run pytest
- id: clang-format

View File

@ -31,7 +31,7 @@ jobs:
today="$(date --utc '+%Y%m%d')"
TAG="${TAG}${today}"
fi
echo "::set-output name=tag::${TAG}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length
deploy-pypi:

View File

@ -18,7 +18,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v6
with:
days-before-pr-stale: 90
days-before-pr-close: 7
@ -38,7 +38,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v6
with:
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@ -1,8 +0,0 @@
---
ports:
- port: 6052
onOpen: open-preview
tasks:
# yamllint disable-line rule:line-length
- before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup
command: python -m esphome dashboard config

View File

@ -3,15 +3,15 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/ambv/black
rev: 22.6.0
rev: 22.10.0
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((esphome|script|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies:
@ -27,7 +27,7 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v3.0.0
rev: v3.2.0
hooks:
- id: pyupgrade
args: [--py39-plus]

View File

@ -13,6 +13,7 @@ esphome/core/* @esphome/core
# Integrations
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_mini/* @ncareau
@ -57,12 +58,14 @@ esphome/components/cse7761/* @berfenger
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daikin_brc/* @hagak
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
@ -76,8 +79,10 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
@ -106,6 +111,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/interval/* @esphome/core
esphome/components/json/* @OttoWinter
esphome/components/kalman_combinator/* @Cat-Ion
esphome/components/lcd_menu/* @numo68
esphome/components/ledc/* @OttoWinter
esphome/components/light/* @esphome/core
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
@ -180,6 +186,8 @@ esphome/components/rc522_spi/* @glmnet
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
@ -254,6 +262,7 @@ esphome/components/wake_on_lan/* @willwill2will54
esphome/components/web_server_base/* @OttoWinter
esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wl_134/* @hobbypunk90
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs

View File

@ -46,7 +46,7 @@ RUN \
# Ubuntu python3-pip is missing wheel
pip3 install --no-cache-dir \
wheel==0.37.1 \
platformio==6.0.2 \
platformio==6.1.5 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \

View File

@ -4,6 +4,7 @@ import logging
import os
import re
import sys
import time
from datetime import datetime
from esphome import const, writer, yaml_util
@ -22,6 +23,9 @@ from esphome.const import (
CONF_ESPHOME,
CONF_PLATFORMIO_OPTIONS,
CONF_SUBSTITUTIONS,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
SECRETS_FILES,
)
from esphome.core import CORE, EsphomeError, coroutine
@ -101,11 +105,11 @@ def run_miniterm(config, port):
if CONF_LOGGER not in config:
_LOGGER.info("Logger is not enabled. Not starting UART logs.")
return
return 1
baud_rate = config["logger"][CONF_BAUD_RATE]
if baud_rate == 0:
_LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.")
return
return 1
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
backtrace_state = False
@ -119,25 +123,34 @@ def run_miniterm(config, port):
ser.dtr = False
ser.rts = False
with ser:
while True:
try:
raw = ser.readline()
except serial.SerialException:
_LOGGER.error("Serial port closed!")
return
line = (
raw.replace(b"\r", b"")
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
time = datetime.now().time().strftime("[%H:%M:%S]")
message = time + line
safe_print(message)
tries = 0
while tries < 5:
try:
with ser:
while True:
try:
raw = ser.readline()
except serial.SerialException:
_LOGGER.error("Serial port closed!")
return 0
line = (
raw.replace(b"\r", b"")
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
time_str = datetime.now().time().strftime("[%H:%M:%S]")
message = time_str + line
safe_print(message)
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException:
tries += 1
time.sleep(1)
if tries >= 5:
_LOGGER.error("Could not connect to serial port %s", port)
return 1
def wrap_to_code(name, comp):
@ -258,9 +271,21 @@ def upload_using_esptool(config, port):
def upload_program(config, args, host):
# if upload is to a serial port use platformio, otherwise assume ota
if get_port_type(host) == "SERIAL":
return upload_using_esptool(config, host)
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
return upload_using_esptool(config, host)
if CORE.target_platform in (PLATFORM_RP2040):
from esphome import platformio_api
upload_args = ["-t", "upload"]
if args.device is not None:
upload_args += ["--upload-port", args.device]
return platformio_api.run_platformio_cli_run(
config, CORE.verbose, *upload_args
)
return 1 # Unknown target platform
from esphome import espota2
@ -280,8 +305,7 @@ def show_logs(config, args, port):
if "logger" not in config:
raise EsphomeError("Logger is not configured!")
if get_port_type(port) == "SERIAL":
run_miniterm(config, port)
return 0
return run_miniterm(config, port)
if get_port_type(port) == "NETWORK" and "api" in config:
from esphome.components.api.client import run_logs

View File

@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from esphome.const import CONF_ID
DEPENDENCIES = ["spi"]
MULTI_CONF = True
CODEOWNERS = ["@DeerMaximum"]
adc128s102_ns = cg.esphome_ns.namespace("adc128s102")
ADC128S102 = adc128s102_ns.class_("ADC128S102", cg.Component, spi.SPIDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADC128S102),
}
).extend(spi.spi_device_schema(cs_pin_required=True))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)

View File

@ -0,0 +1,35 @@
#include "adc128s102.h"
#include "esphome/core/log.h"
namespace esphome {
namespace adc128s102 {
static const char *const TAG = "adc128s102";
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
void ADC128S102::setup() {
ESP_LOGCONFIG(TAG, "Setting up adc128s102");
this->spi_setup();
}
void ADC128S102::dump_config() {
ESP_LOGCONFIG(TAG, "ADC128S102:");
LOG_PIN(" CS Pin:", this->cs_);
}
uint16_t ADC128S102::read_data(uint8_t channel) {
uint8_t control = channel << 3;
this->enable();
uint8_t adc_primary_byte = this->transfer_byte(control);
uint8_t adc_secondary_byte = this->transfer_byte(0x00);
this->disable();
uint16_t digital_value = adc_primary_byte << 8 | adc_secondary_byte;
return digital_value;
}
} // namespace adc128s102
} // namespace esphome

View File

@ -0,0 +1,23 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace adc128s102 {
class ADC128S102 : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_10MHZ> {
public:
ADC128S102() = default;
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
uint16_t read_data(uint8_t channel);
};
} // namespace adc128s102
} // namespace esphome

View File

@ -0,0 +1,35 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import CONF_ID, CONF_CHANNEL
from .. import adc128s102_ns, ADC128S102
AUTO_LOAD = ["voltage_sampler"]
DEPENDENCIES = ["adc128s102"]
ADC128S102Sensor = adc128s102_ns.class_(
"ADC128S102Sensor",
sensor.Sensor,
cg.PollingComponent,
voltage_sampler.VoltageSampler,
)
CONF_ADC128S102_ID = "adc128s102_id"
CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(ADC128S102Sensor),
cv.GenerateID(CONF_ADC128S102_ID): cv.use_id(ADC128S102),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_CHANNEL],
)
await cg.register_parented(var, config[CONF_ADC128S102_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)

View File

@ -0,0 +1,24 @@
#include "adc128s102_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace adc128s102 {
static const char *const TAG = "adc128s102.sensor";
ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {}
float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; }
void ADC128S102Sensor::dump_config() {
LOG_SENSOR("", "ADC128S102 Sensor", this);
ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_);
LOG_UPDATE_INTERVAL(this);
}
float ADC128S102Sensor::sample() { return this->parent_->read_data(this->channel_); }
void ADC128S102Sensor::update() { this->publish_state(this->sample()); }
} // namespace adc128s102
} // namespace esphome

View File

@ -0,0 +1,29 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "../adc128s102.h"
namespace esphome {
namespace adc128s102 {
class ADC128S102Sensor : public PollingComponent,
public Parented<ADC128S102>,
public sensor::Sensor,
public voltage_sampler::VoltageSampler {
public:
ADC128S102Sensor(uint8_t channel);
void update() override;
void dump_config() override;
float get_setup_priority() const override;
float sample() override;
protected:
uint8_t channel_;
};
} // namespace adc128s102
} // namespace esphome

View File

@ -204,6 +204,8 @@ message DeviceInfoResponse {
uint32 webserver_port = 10;
uint32 bluetooth_proxy_version = 11;
string manufacturer = 12;
}
message ListEntitiesRequest {

View File

@ -932,6 +932,11 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time();
#if defined(USE_ESP8266) || defined(USE_ESP32)
resp.manufacturer = "Espressif";
#elif defined(USE_RP2040)
resp.manufacturer = "Raspberry Pi";
#endif
resp.model = ESPHOME_BOARD;
#ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;

View File

@ -576,6 +576,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
this->project_version = value.as_string();
return true;
}
case 12: {
this->manufacturer = value.as_string();
return true;
}
default:
return false;
}
@ -592,6 +596,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(9, this->project_version);
buffer.encode_uint32(10, this->webserver_port);
buffer.encode_uint32(11, this->bluetooth_proxy_version);
buffer.encode_string(12, this->manufacturer);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const {
@ -642,6 +647,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
sprintf(buffer, "%u", this->bluetooth_proxy_version);
out.append(buffer);
out.append("\n");
out.append(" manufacturer: ");
out.append("'").append(this->manufacturer).append("'");
out.append("\n");
out.append("}");
}
#endif

View File

@ -273,6 +273,7 @@ class DeviceInfoResponse : public ProtoMessage {
std::string project_version{};
uint32_t webserver_port{0};
uint32_t bluetooth_proxy_version{0};
std::string manufacturer{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;

View File

@ -75,7 +75,7 @@ class APIServer : public Component, public Controller {
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
#ifdef USE_BLUETOOTH_PROXY
void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call);
void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error = ESP_OK);
void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
void send_bluetooth_connections_free(uint8_t free, uint8_t limit);
void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call);
void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call);

View File

@ -8,6 +8,7 @@ CODEOWNERS = ["@OttoWinter"]
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.only_with_arduino,
cv.only_on(["esp32", "esp8266"]),
)

View File

@ -47,11 +47,12 @@ void BLEClient::set_enabled(bool enabled) {
this->enabled = enabled;
}
void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) {
bool all_established = this->all_nodes_established_();
BLEClientBase::gattc_event_handler(event, esp_gattc_if, param);
if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
return false;
for (auto *node : this->nodes_)
node->gattc_event_handler(event, esp_gattc_if, param);
@ -62,6 +63,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.clear();
}
return true;
}
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {

View File

@ -50,7 +50,7 @@ class BLEClient : public BLEClientBase {
void dump_config() override;
void loop() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;

View File

@ -7,19 +7,56 @@ AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"]
CODEOWNERS = ["@jesserockz"]
CONF_CONNECTIONS = "connections"
MAX_CONNECTIONS = 3
bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy")
BluetoothProxy = bluetooth_proxy_ns.class_(
"BluetoothProxy", esp32_ble_client.BLEClientBase
"BluetoothProxy", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
)
BluetoothConnection = bluetooth_proxy_ns.class_(
"BluetoothConnection", esp32_ble_client.BLEClientBase
)
CONFIG_SCHEMA = cv.Schema(
CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.Optional(CONF_ACTIVE, default=False): cv.boolean,
cv.GenerateID(): cv.declare_id(BluetoothConnection),
}
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
).extend(cv.COMPONENT_SCHEMA)
def validate_connections(config):
if CONF_CONNECTIONS in config:
if not config[CONF_ACTIVE]:
raise cv.Invalid(
"Connections can only be used if the proxy is set to active"
)
else:
if config[CONF_ACTIVE]:
conf = config.copy()
conf[CONF_CONNECTIONS] = [
CONNECTION_SCHEMA({}) for _ in range(MAX_CONNECTIONS)
]
return conf
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.Optional(CONF_ACTIVE, default=False): cv.boolean,
cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=MAX_CONNECTIONS),
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_connections,
)
async def to_code(config):
@ -27,7 +64,12 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_ble_device(var, config)
await esp32_ble_tracker.register_client(var, config)
for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf)
cg.add(var.register_connection(connection_var))
await esp32_ble_tracker.register_client(connection_var, connection_conf)
cg.add_define("USE_BLUETOOTH_PROXY")

View File

@ -0,0 +1,281 @@
#include "bluetooth_connection.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#include "bluetooth_proxy.h"
namespace esphome {
namespace bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy.connection";
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
return false;
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason);
this->set_address(0);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
break;
}
case ESP_GATTC_OPEN_EVT: {
if (param->open.conn_id != this->conn_id_)
break;
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status);
this->set_address(0);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
}
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (param->search_cmpl.conn_id != this->conn_id_)
break;
api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
break;
}
case ESP_GATTC_READ_DESCR_EVT:
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->conn_id_)
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->read.handle, param->read.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status);
break;
}
api::BluetoothGATTReadResponse resp;
resp.address = this->address_;
resp.handle = param->read.handle;
resp.data.reserve(param->read.value_len);
for (uint16_t i = 0; i < param->read.value_len; i++) {
resp.data.push_back(param->read.value[i]);
}
api::global_api_server->send_bluetooth_gatt_read_response(resp);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.conn_id != this->conn_id_)
break;
if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->write.handle, param->write.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status);
break;
}
api::BluetoothGATTWriteResponse resp;
resp.address = this->address_;
resp.handle = param->write.handle;
api::global_api_server->send_bluetooth_gatt_write_response(resp);
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (this->get_characteristic(param->unreg_for_notify.handle) == nullptr) // No conn_id in this event
break;
if (param->unreg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
param->unreg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle,
param->unreg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (this->get_characteristic(param->reg_for_notify.handle) == nullptr) // No conn_id in this event
break;
if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle,
param->reg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->reg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.conn_id != this->conn_id_)
break;
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(),
param->notify.handle);
api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_;
resp.handle = param->notify.handle;
resp.data.reserve(param->notify.value_len);
for (uint16_t i = 0; i < param->notify.value_len; i++) {
resp.data.push_back(param->notify.value[i]);
}
api::global_api_server->send_bluetooth_gatt_notify_data_response(resp);
break;
}
default:
break;
}
return true;
}
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
auto *characteristic = this->get_characteristic(handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not found.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_INVALID_HANDLE;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic %s", this->connection_index_, this->address_str_.c_str(),
characteristic->uuid.to_string().c_str());
esp_err_t err =
esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, characteristic->handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
auto *characteristic = this->get_characteristic(handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not found.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_INVALID_HANDLE;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic %s", this->connection_index_, this->address_str_.c_str(),
characteristic->uuid.to_string().c_str());
auto err = characteristic->write_value((uint8_t *) data.data(), data.size(),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
auto *descriptor = this->get_descriptor(handle);
if (descriptor == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not found.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_INVALID_HANDLE;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor %s", this->connection_index_, this->address_str_.c_str(),
descriptor->uuid.to_string().c_str());
esp_err_t err =
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data) {
if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
auto *descriptor = this->get_descriptor(handle);
if (descriptor == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not found.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_INVALID_HANDLE;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor %s", this->connection_index_, this->address_str_.c_str(),
descriptor->uuid.to_string().c_str());
auto err =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, data.size(),
(uint8_t *) data.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
auto *characteristic = this->get_characteristic(handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not found.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_INVALID_HANDLE;
}
if (enable) {
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications %s", this->connection_index_,
this->address_str_.c_str(), characteristic->uuid.to_string().c_str());
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
} else {
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications %s", this->connection_index_,
this->address_str_.c_str(), characteristic->uuid.to_string().c_str());
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
}
return ESP_OK;
}
} // namespace bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32

View File

@ -0,0 +1,34 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_client/ble_client_base.h"
namespace esphome {
namespace bluetooth_proxy {
class BluetoothProxy;
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
public:
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
esp_err_t read_characteristic(uint16_t handle);
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
esp_err_t read_descriptor(uint16_t handle);
esp_err_t write_descriptor(uint16_t handle, const std::string &data);
esp_err_t notify_characteristic(uint16_t handle, bool enable);
protected:
friend class BluetoothProxy;
int16_t send_service_{-1};
BluetoothProxy *proxy_;
};
} // namespace bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32

View File

@ -11,13 +11,7 @@ namespace bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy";
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
static const esp_err_t ESP_GATT_WRONG_ADDRESS = -2;
BluetoothProxy::BluetoothProxy() {
global_bluetooth_proxy = this;
this->address_ = 0;
}
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected())
@ -26,10 +20,6 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
device.get_rssi());
this->send_api_packet_(device);
if (this->address_ == 0)
return true;
BLEClientBase::parse_device(device);
return true;
}
@ -57,170 +47,101 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
api::global_api_server->send_bluetooth_le_advertisement(resp);
}
void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
BLEClientBase::gattc_event_handler(event, gattc_if, param);
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_,
param->disconnect.reason);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
this->address_ = 0;
}
case ESP_GATTC_OPEN_EVT: {
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, param->open.status);
break;
void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG, " Active: %s", YESNO(this->active_));
}
void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected()) {
for (auto *connection : this->connections_) {
if (connection->get_address() != 0) {
connection->disconnect();
}
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
break;
}
case ESP_GATTC_READ_DESCR_EVT:
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->conn_id_)
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char/descriptor at handle 0x%2X, status=%d", param->read.handle,
param->read.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status);
break;
return;
}
for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->services_.size()) {
connection->send_service_ = -1;
api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address());
} else if (connection->send_service_ >= 0) {
auto &service = connection->services_[connection->send_service_];
api::BluetoothGATTGetServicesResponse resp;
resp.address = connection->get_address();
api::BluetoothGATTService service_resp;
service_resp.uuid = {service->uuid.get_128bit_high(), service->uuid.get_128bit_low()};
service_resp.handle = service->start_handle;
for (auto &characteristic : service->characteristics) {
api::BluetoothGATTCharacteristic characteristic_resp;
characteristic_resp.uuid = {characteristic->uuid.get_128bit_high(), characteristic->uuid.get_128bit_low()};
characteristic_resp.handle = characteristic->handle;
characteristic_resp.properties = characteristic->properties;
for (auto &descriptor : characteristic->descriptors) {
api::BluetoothGATTDescriptor descriptor_resp;
descriptor_resp.uuid = {descriptor->uuid.get_128bit_high(), descriptor->uuid.get_128bit_low()};
descriptor_resp.handle = descriptor->handle;
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
}
service_resp.characteristics.push_back(std::move(characteristic_resp));
}
api::BluetoothGATTReadResponse resp;
resp.address = this->address_;
resp.handle = param->read.handle;
resp.data.reserve(param->read.value_len);
for (uint16_t i = 0; i < param->read.value_len; i++) {
resp.data.push_back(param->read.value[i]);
}
api::global_api_server->send_bluetooth_gatt_read_response(resp);
break;
resp.services.push_back(std::move(service_resp));
api::global_api_server->send_bluetooth_gatt_services(resp);
connection->send_service_++;
}
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.conn_id != this->conn_id_)
break;
if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error writing char/descriptor at handle 0x%2X, status=%d", param->write.handle,
param->write.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status);
break;
}
api::BluetoothGATTWriteResponse resp;
resp.address = this->address_;
resp.handle = param->write.handle;
api::global_api_server->send_bluetooth_gatt_write_response(resp);
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (param->unreg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error unregistering notifications for handle 0x%2X, status=%d", param->unreg_for_notify.handle,
param->unreg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle,
param->unreg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error registering notifications for handle 0x%2X, status=%d", param->reg_for_notify.handle,
param->reg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle,
param->reg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->reg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.conn_id != this->conn_id_)
break;
ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT: handle=0x%2X", param->notify.handle);
api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_;
resp.handle = param->notify.handle;
resp.data.reserve(param->notify.value_len);
for (uint16_t i = 0; i < param->notify.value_len; i++) {
resp.data.push_back(param->notify.value[i]);
}
api::global_api_server->send_bluetooth_gatt_notify_data_response(resp);
break;
}
default:
break;
}
}
void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); }
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (auto *connection : this->connections_) {
if (connection->get_address() == address)
return connection;
}
void BluetoothProxy::loop() {
BLEClientBase::loop();
if (this->state_ != espbt::ClientState::IDLE && !api::global_api_server->is_connected()) {
ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str());
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s err=%d", this->address_str().c_str(), err);
if (!reserve)
return nullptr;
for (auto *connection : this->connections_) {
if (connection->get_address() == 0) {
connection->set_address(address);
return connection;
}
}
if (this->send_service_ == this->services_.size()) {
this->send_service_ = -1;
api::global_api_server->send_bluetooth_gatt_services_done(this->address_);
} else if (this->send_service_ >= 0) {
auto &service = this->services_[this->send_service_];
api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_;
api::BluetoothGATTService service_resp;
service_resp.uuid = {service->uuid.get_128bit_high(), service->uuid.get_128bit_low()};
service_resp.handle = service->start_handle;
for (auto &characteristic : service->characteristics) {
api::BluetoothGATTCharacteristic characteristic_resp;
characteristic_resp.uuid = {characteristic->uuid.get_128bit_high(), characteristic->uuid.get_128bit_low()};
characteristic_resp.handle = characteristic->handle;
characteristic_resp.properties = characteristic->properties;
for (auto &descriptor : characteristic->descriptors) {
api::BluetoothGATTDescriptor descriptor_resp;
descriptor_resp.uuid = {descriptor->uuid.get_128bit_high(), descriptor->uuid.get_128bit_low()};
descriptor_resp.handle = descriptor->handle;
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
}
service_resp.characteristics.push_back(std::move(characteristic_resp));
}
resp.services.push_back(std::move(service_resp));
api::global_api_server->send_bluetooth_gatt_services(resp);
this->send_service_++;
}
return nullptr;
}
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
switch (msg.request_type) {
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
this->address_ = msg.address;
this->set_state(espbt::ClientState::SEARCHING);
auto *connection = this->get_connection_(msg.address, true);
if (connection == nullptr) {
ESP_LOGW(TAG, "No free connections available");
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
return;
}
ESP_LOGV(TAG, "[%d] [%s] Searching to connect", connection->get_connection_index(),
connection->address_str().c_str());
connection->set_state(espbt::ClientState::SEARCHING);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
break;
}
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: {
if (this->state() != espbt::ClientState::IDLE) {
ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str());
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s err=%d", this->address_str().c_str(), err);
}
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
return;
}
if (connection->state() != espbt::ClientState::IDLE) {
connection->disconnect();
} else {
connection->set_address(0);
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
}
break;
}
@ -231,170 +152,88 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
}
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for read GATT characteristic request");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS);
return;
}
auto *characteristic = this->get_characteristic(msg.handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT characteristic, not found.");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE);
return;
}
ESP_LOGV(TAG, "Reading GATT characteristic %s", characteristic->uuid.to_string().c_str());
esp_err_t err =
esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, characteristic->handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err);
auto err = connection->read_characteristic(msg.handle);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
}
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for write GATT characteristic request");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS);
return;
}
auto *characteristic = this->get_characteristic(msg.handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT characteristic, not found.");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE);
return;
}
ESP_LOGV(TAG, "Writing GATT characteristic %s", characteristic->uuid.to_string().c_str());
auto err = characteristic->write_value((uint8_t *) msg.data.data(), msg.data.size(),
msg.response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP);
if (err != ERR_OK) {
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
}
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for read GATT characteristic descriptor request");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS);
return;
}
auto *descriptor = this->get_descriptor(msg.handle);
if (descriptor == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not found.");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE);
return;
}
ESP_LOGV(TAG, "Reading GATT characteristic descriptor %s -> %s", descriptor->characteristic->uuid.to_string().c_str(),
descriptor->uuid.to_string().c_str());
esp_err_t err =
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err);
auto err = connection->read_descriptor(msg.handle);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
}
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for write GATT characteristic descriptor request");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS);
return;
}
auto *descriptor = this->get_descriptor(msg.handle);
if (descriptor == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not found.");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE);
return;
}
ESP_LOGV(TAG, "Writing GATT characteristic descriptor %s -> %s", descriptor->characteristic->uuid.to_string().c_str(),
descriptor->uuid.to_string().c_str());
esp_err_t err =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, msg.data.size(),
(uint8_t *) msg.data.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, err=%d", err);
auto err = connection->write_descriptor(msg.handle, msg.data);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
}
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot get GATT services, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr || !connection->connected()) {
ESP_LOGW(TAG, "Cannot get GATT services, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for service list request");
api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_WRONG_ADDRESS);
if (connection->services_.empty()) {
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
api::global_api_server->send_bluetooth_gatt_services_done(msg.address);
return;
}
this->send_service_ = 0;
if (connection->send_service_ == -1) // Don't start sending services again if we're already sending them
connection->send_service_ = 0;
}
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
if (this->state_ != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot configure notify, not connected.");
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
if (this->address_ != msg.address) {
ESP_LOGW(TAG, "Address mismatch for notify");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS);
return;
}
auto *characteristic = this->get_characteristic(msg.handle);
if (characteristic == nullptr) {
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not found.");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE);
return;
}
esp_err_t err;
if (msg.enable) {
err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, err=%d", err);
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
} else {
err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_unregister_for_notify failed, err=%d", err);
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
auto err = connection->notify_characteristic(msg.handle, msg.enable);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
}
}

View File

@ -11,22 +11,26 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include <map>
#include "bluetooth_connection.h"
namespace esphome {
namespace bluetooth_proxy {
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
using namespace esp32_ble_client;
class BluetoothProxy : public BLEClientBase {
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public:
BluetoothProxy();
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
void loop() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void register_connection(BluetoothConnection *connection) {
this->connections_.push_back(connection);
connection->proxy_ = this;
}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg);
@ -36,8 +40,16 @@ class BluetoothProxy : public BLEClientBase {
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg);
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg);
int get_bluetooth_connections_free() { return this->state_ == espbt::ClientState::IDLE ? 1 : 0; }
int get_bluetooth_connections_limit() { return 1; }
int get_bluetooth_connections_free() {
int free = 0;
for (auto *connection : this->connections_) {
if (connection->address_ == 0) {
free++;
}
}
return free;
}
int get_bluetooth_connections_limit() { return this->connections_.size(); }
void set_active(bool active) { this->active_ = active; }
bool has_active() { return this->active_; }
@ -45,8 +57,12 @@ class BluetoothProxy : public BLEClientBase {
protected:
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
int16_t send_service_{-1};
bool active_;
std::vector<BluetoothConnection *> connections_{};
};
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -22,6 +22,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
cv.only_on(["esp32", "esp8266"]),
)

View File

@ -13,6 +13,7 @@ using namespace esphome::cover;
CoverTraits CurrentBasedCover::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_supports_toggle(true);
traits.set_is_assumed_state(false);
return traits;
}
@ -20,6 +21,20 @@ void CurrentBasedCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->direction_idle_();
}
if (call.get_toggle().has_value()) {
if (this->current_operation != COVER_OPERATION_IDLE) {
this->start_direction_(COVER_OPERATION_IDLE);
this->publish_state();
} else {
if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
this->target_position_ = COVER_OPEN;
this->start_direction_(COVER_OPERATION_OPENING);
} else {
this->target_position_ = COVER_CLOSED;
this->start_direction_(COVER_OPERATION_CLOSING);
}
}
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == this->position) {
@ -202,9 +217,11 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) {
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = this->close_trigger_;
break;
default:

View File

@ -89,6 +89,8 @@ class CurrentBasedCover : public cover::Cover, public Component {
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
float target_position_{0};
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
};
} // namespace current_based

View File

@ -0,0 +1 @@
CODEOWNERS = ["@hagak"]

View File

@ -0,0 +1,24 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate_ir
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
CONF_USE_FAHRENHEIT = "use_fahrenheit"
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DaikinBrcClimate),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))

View File

@ -0,0 +1,273 @@
#include "daikin_brc.h"
#include "esphome/components/remote_base/remote_base.h"
namespace esphome {
namespace daikin_brc {
static const char *const TAG = "daikin_brc.climate";
void DaikinBrcClimate::control(const climate::ClimateCall &call) {
this->mode_button_ = 0x00;
if (call.get_mode().has_value()) {
// Need to determine if this is call due to Mode button pressed so that we can set the Mode button byte
this->mode_button_ = DAIKIN_BRC_IR_MODE_BUTTON;
}
ClimateIR::control(call);
}
void DaikinBrcClimate::transmit_state() {
uint8_t remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE] = {0x11, 0xDA, 0x17, 0x18, 0x04, 0x00, 0x1E, 0x11,
0xDA, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00};
remote_state[12] = this->alt_mode_();
remote_state[13] = this->mode_button_;
remote_state[14] = this->operation_mode_();
remote_state[17] = this->temperature_();
remote_state[18] = this->fan_speed_swing_();
// Calculate checksum
for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1; i++) {
remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1] += remote_state[i];
}
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(DAIKIN_BRC_IR_FREQUENCY);
data->mark(DAIKIN_BRC_HEADER_MARK);
data->space(DAIKIN_BRC_HEADER_SPACE);
for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BRC_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
}
}
data->mark(DAIKIN_BRC_BIT_MARK);
data->space(DAIKIN_BRC_MESSAGE_SPACE);
data->mark(DAIKIN_BRC_HEADER_MARK);
data->space(DAIKIN_BRC_HEADER_SPACE);
for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BRC_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
}
}
data->mark(DAIKIN_BRC_BIT_MARK);
data->space(0);
transmit.perform();
}
uint8_t DaikinBrcClimate::alt_mode_() {
uint8_t alt_mode = 0x00;
switch (this->mode) {
case climate::CLIMATE_MODE_DRY:
alt_mode = 0x23;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
alt_mode = 0x63;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_HEAT:
default:
alt_mode = 0x73;
break;
}
return alt_mode;
}
uint8_t DaikinBrcClimate::operation_mode_() {
uint8_t operating_mode = DAIKIN_BRC_MODE_ON;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
operating_mode |= DAIKIN_BRC_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
operating_mode |= DAIKIN_BRC_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
operating_mode |= DAIKIN_BRC_MODE_HEAT;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
operating_mode |= DAIKIN_BRC_MODE_AUTO;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
operating_mode |= DAIKIN_BRC_MODE_FAN;
break;
case climate::CLIMATE_MODE_OFF:
default:
operating_mode = DAIKIN_BRC_MODE_OFF;
break;
}
return operating_mode;
}
uint8_t DaikinBrcClimate::fan_speed_swing_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_BRC_FAN_1;
break;
case climate::CLIMATE_FAN_MEDIUM:
fan_speed = DAIKIN_BRC_FAN_2;
break;
case climate::CLIMATE_FAN_HIGH:
fan_speed = DAIKIN_BRC_FAN_3;
break;
default:
fan_speed = DAIKIN_BRC_FAN_1;
}
// If swing is enabled switch first 4 bits to 1111
switch (this->swing_mode) {
case climate::CLIMATE_SWING_BOTH:
fan_speed |= DAIKIN_BRC_IR_SWING_ON;
break;
default:
fan_speed |= DAIKIN_BRC_IR_SWING_OFF;
break;
}
return fan_speed;
}
uint8_t DaikinBrcClimate::temperature_() {
// Force special temperatures depending on the mode
switch (this->mode) {
case climate::CLIMATE_MODE_FAN_ONLY:
case climate::CLIMATE_MODE_DRY:
if (this->fahrenheit_) {
return DAIKIN_BRC_IR_DRY_FAN_TEMP_F;
}
return DAIKIN_BRC_IR_DRY_FAN_TEMP_C;
case climate::CLIMATE_MODE_HEAT_COOL:
default:
uint8_t temperature;
// Temperature in remote is in F
if (this->fahrenheit_) {
temperature = (uint8_t) roundf(
clamp<float>(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
} else {
temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1;
}
return temperature;
}
}
bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) {
ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum);
return false;
}
uint8_t mode = frame[7];
if (mode & DAIKIN_BRC_MODE_ON) {
switch (mode & 0xF0) {
case DAIKIN_BRC_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case DAIKIN_BRC_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case DAIKIN_BRC_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case DAIKIN_BRC_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case DAIKIN_BRC_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
}
uint8_t temperature = frame[10];
float temperature_c;
if (this->fahrenheit_) {
temperature_c = clamp<float>(((temperature - 32) / 1.8), DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C);
} else {
temperature_c = (temperature >> 1) + 9;
}
this->target_temperature = temperature_c;
uint8_t fan_mode = frame[11];
uint8_t swing_mode = frame[11];
switch (swing_mode & 0xF) {
case DAIKIN_BRC_IR_SWING_ON:
this->swing_mode = climate::CLIMATE_SWING_BOTH;
break;
case DAIKIN_BRC_IR_SWING_OFF:
this->swing_mode = climate::CLIMATE_SWING_OFF;
break;
}
switch (fan_mode & 0xF0) {
case DAIKIN_BRC_FAN_1:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case DAIKIN_BRC_FAN_2:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case DAIKIN_BRC_FAN_3:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
}
this->publish_state();
return true;
}
bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {};
if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) {
return false;
}
}
state_frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11)
return false;
} else if (pos == 1) {
// frame header
if (byte != 0xDA)
return false;
} else if (pos == 2) {
// frame header
if (byte != 0x17)
return false;
} else if (pos == 3) {
// frame header
if (byte != 0x18)
return false;
} else if (pos == 4) {
// frame type
if (byte != 0x00)
return false;
}
}
return this->parse_state_frame_(state_frame);
}
} // namespace daikin_brc
} // namespace esphome

View File

@ -0,0 +1,82 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome {
namespace daikin_brc {
// Values for Daikin BRC4CXXX IR Controllers
// Temperature
const uint8_t DAIKIN_BRC_TEMP_MIN_F = 60; // fahrenheit
const uint8_t DAIKIN_BRC_TEMP_MAX_F = 90; // fahrenheit
const float DAIKIN_BRC_TEMP_MIN_C = (DAIKIN_BRC_TEMP_MIN_F - 32) / 1.8; // fahrenheit
const float DAIKIN_BRC_TEMP_MAX_C = (DAIKIN_BRC_TEMP_MAX_F - 32) / 1.8; // fahrenheit
// Modes
const uint8_t DAIKIN_BRC_MODE_AUTO = 0x30;
const uint8_t DAIKIN_BRC_MODE_COOL = 0x20;
const uint8_t DAIKIN_BRC_MODE_HEAT = 0x10;
const uint8_t DAIKIN_BRC_MODE_DRY = 0x70;
const uint8_t DAIKIN_BRC_MODE_FAN = 0x00;
const uint8_t DAIKIN_BRC_MODE_OFF = 0x00;
const uint8_t DAIKIN_BRC_MODE_ON = 0x01;
// Fan Speed
const uint8_t DAIKIN_BRC_FAN_1 = 0x10;
const uint8_t DAIKIN_BRC_FAN_2 = 0x30;
const uint8_t DAIKIN_BRC_FAN_3 = 0x50;
const uint8_t DAIKIN_BRC_FAN_AUTO = 0xA0;
// IR Transmission
const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000;
const uint32_t DAIKIN_BRC_HEADER_MARK = 5070;
const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140;
const uint32_t DAIKIN_BRC_BIT_MARK = 370;
const uint32_t DAIKIN_BRC_ONE_SPACE = 1780;
const uint32_t DAIKIN_BRC_ZERO_SPACE = 710;
const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410;
const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius.
const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_C = (17 - 9) * 2; // Dry/Fan mode is always 17 Celsius.
const uint8_t DAIKIN_BRC_IR_SWING_ON = 0x5;
const uint8_t DAIKIN_BRC_IR_SWING_OFF = 0x6;
const uint8_t DAIKIN_BRC_IR_MODE_BUTTON = 0x4; // This is set after a mode action
// State Frame size
const uint8_t DAIKIN_BRC_STATE_FRAME_SIZE = 15;
// Preamble size
const uint8_t DAIKIN_BRC_PREAMBLE_SIZE = 7;
// Transmit Frame size - includes a preamble
const uint8_t DAIKIN_BRC_TRANSMIT_FRAME_SIZE = DAIKIN_BRC_PREAMBLE_SIZE + DAIKIN_BRC_STATE_FRAME_SIZE;
class DaikinBrcClimate : public climate_ir::ClimateIR {
public:
DaikinBrcClimate()
: climate_ir::ClimateIR(DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C, 0.5f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH}) {}
/// Set use of Fahrenheit units
void set_fahrenheit(bool value) {
this->fahrenheit_ = value;
this->temperature_step_ = value ? 0.5f : 1.0f;
}
protected:
uint8_t mode_button_ = 0x00;
// Capture if the MODE was changed
void control(const climate::ClimateCall &call) override;
// Transmit via IR the state of this climate controller.
void transmit_state() override;
uint8_t alt_mode_();
uint8_t operation_mode_();
uint8_t fan_speed_swing_();
uint8_t temperature_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
bool fahrenheit_{false};
};
} // namespace daikin_brc
} // namespace esphome

View File

@ -0,0 +1,430 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation, core
from esphome.const import (
CONF_ID,
CONF_TYPE,
CONF_TRIGGER_ID,
CONF_ON_VALUE,
CONF_COMMAND,
CONF_NUMBER,
CONF_FORMAT,
CONF_MODE,
CONF_ACTIVE,
)
from esphome.automation import maybe_simple_id
from esphome.components.select import Select
from esphome.components.number import Number
from esphome.components.switch import Switch
CODEOWNERS = ["@numo68"]
display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base")
CONF_DISPLAY_ID = "display_id"
CONF_ROTARY = "rotary"
CONF_JOYSTICK = "joystick"
CONF_LABEL = "label"
CONF_MENU = "menu"
CONF_BACK = "back"
CONF_TEXT = "text"
CONF_SELECT = "select"
CONF_SWITCH = "switch"
CONF_CUSTOM = "custom"
CONF_ITEMS = "items"
CONF_ON_TEXT = "on_text"
CONF_OFF_TEXT = "off_text"
CONF_VALUE_LAMBDA = "value_lambda"
CONF_IMMEDIATE_EDIT = "immediate_edit"
CONF_ROOT_ITEM_ID = "root_item_id"
CONF_ON_ENTER = "on_enter"
CONF_ON_LEAVE = "on_leave"
CONF_ON_NEXT = "on_next"
CONF_ON_PREV = "on_prev"
DisplayMenuComponent = display_menu_base_ns.class_("DisplayMenuComponent", cg.Component)
MenuItem = display_menu_base_ns.class_("MenuItem")
MenuItemConstPtr = MenuItem.operator("ptr").operator("const")
MenuItemMenu = display_menu_base_ns.class_("MenuItemMenu")
MenuItemSelect = display_menu_base_ns.class_("MenuItemSelect")
MenuItemNumber = display_menu_base_ns.class_("MenuItemNumber")
MenuItemSwitch = display_menu_base_ns.class_("MenuItemSwitch")
MenuItemCommand = display_menu_base_ns.class_("MenuItemCommand")
MenuItemCustom = display_menu_base_ns.class_("MenuItemCustom")
UpAction = display_menu_base_ns.class_("UpAction", automation.Action)
DownAction = display_menu_base_ns.class_("DownAction", automation.Action)
LeftAction = display_menu_base_ns.class_("LeftAction", automation.Action)
RightAction = display_menu_base_ns.class_("RightAction", automation.Action)
EnterAction = display_menu_base_ns.class_("EnterAction", automation.Action)
ShowAction = display_menu_base_ns.class_("ShowAction", automation.Action)
HideAction = display_menu_base_ns.class_("HideAction", automation.Action)
ShowMainAction = display_menu_base_ns.class_("ShowMainAction", automation.Action)
IsActiveCondition = display_menu_base_ns.class_(
"IsActiveCondition", automation.Condition
)
MULTI_CONF = True
MenuItemType = display_menu_base_ns.enum("MenuItemType")
MENU_ITEM_TYPES = {
CONF_LABEL: MenuItemType.MENU_ITEM_LABEL,
CONF_MENU: MenuItemType.MENU_ITEM_MENU,
CONF_BACK: MenuItemType.MENU_ITEM_BACK,
CONF_SELECT: MenuItemType.MENU_ITEM_SELECT,
CONF_NUMBER: MenuItemType.MENU_ITEM_NUMBER,
CONF_SWITCH: MenuItemType.MENU_ITEM_SWITCH,
CONF_COMMAND: MenuItemType.MENU_ITEM_COMMAND,
CONF_CUSTOM: MenuItemType.MENU_ITEM_CUSTOM,
}
MENU_ITEMS_WITH_SPECIALIZED_CLASSES = (
CONF_MENU,
CONF_SELECT,
CONF_NUMBER,
CONF_SWITCH,
CONF_COMMAND,
CONF_CUSTOM,
)
MenuMode = display_menu_base_ns.enum("MenuMode")
MENU_MODES = {
CONF_ROTARY: MenuMode.MENU_MODE_ROTARY,
CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
}
DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
"DisplayMenuOnEnterTrigger", automation.Trigger
)
DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
"DisplayMenuOnLeaveTrigger", automation.Trigger
)
DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
"DisplayMenuOnValueTrigger", automation.Trigger
)
DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
"DisplayMenuOnNextTrigger", automation.Trigger
)
DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
"DisplayMenuOnPrevTrigger", automation.Trigger
)
def validate_format(format):
if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None:
raise cv.Invalid(
f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided"
)
return format
# Use a simple indirection to circumvent the recursion limitation
def menu_item_schema(value):
return MENU_ITEM_SCHEMA(value)
MENU_ITEM_COMMON_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): cv.templatable(cv.string),
}
)
MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
}
)
MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
}
)
MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
}
)
MENU_ITEM_SCHEMA = cv.typed_schema(
{
CONF_LABEL: MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
}
),
CONF_BACK: MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
}
),
CONF_MENU: MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemMenu),
cv.Required(CONF_ITEMS): cv.All(
cv.ensure_list(menu_item_schema), cv.Length(min=1)
),
}
),
CONF_SELECT: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSelect),
cv.Required(CONF_SELECT): cv.use_id(Select),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_NUMBER: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemNumber),
cv.Required(CONF_NUMBER): cv.use_id(Number),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_FORMAT, default="%.1f"): cv.All(
cv.string_strict,
validate_format,
),
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_SWITCH: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSwitch),
cv.Required(CONF_SWITCH): cv.use_id(Switch),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_ON_TEXT, default="On"): cv.string_strict,
cv.Optional(CONF_OFF_TEXT, default="Off"): cv.string_strict,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_COMMAND: MENU_ITEM_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCommand),
}
),
CONF_CUSTOM: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_ON_NEXT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnNextTrigger
),
}
),
cv.Optional(CONF_ON_PREV): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnPrevTrigger
),
}
),
}
),
},
default_type="label",
lower=True,
)
DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
cv.Required(CONF_ITEMS): cv.All(
cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
),
}
).extend(cv.COMPONENT_SCHEMA)
MENU_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
}
)
@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA)
async def menu_up_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA)
async def menu_down_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA)
async def menu_left_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA)
async def menu_right_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA)
async def menu_enter_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA)
async def menu_show_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA)
async def menu_hide_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA
)
async def menu_show_main_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition(
"display_menu.is_active",
IsActiveCondition,
automation.maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
}
),
)
async def display_menu_is_active_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
async def menu_item_to_code(menu, config, parent):
if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
item = cg.new_Pvariable(config[CONF_ID])
else:
item = cg.new_Pvariable(config[CONF_ID], MENU_ITEM_TYPES[config[CONF_TYPE]])
cg.add(parent.add_item(item))
if CONF_TEXT in config:
if isinstance(config[CONF_TEXT], core.Lambda):
template_ = await cg.templatable(
config[CONF_TEXT], [(MenuItemConstPtr, "it")], cg.std_string
)
cg.add(item.set_text(template_))
else:
cg.add(item.set_text(config[CONF_TEXT]))
if CONF_VALUE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_VALUE_LAMBDA],
[(MenuItemConstPtr, "it")],
return_type=cg.std_string,
)
cg.add(item.set_value_lambda(template_))
if CONF_ITEMS in config:
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, item)
if CONF_IMMEDIATE_EDIT in config:
cg.add(item.set_immediate_edit(config[CONF_IMMEDIATE_EDIT]))
if config[CONF_TYPE] == CONF_SELECT:
var = await cg.get_variable(config[CONF_SELECT])
cg.add(item.set_select_variable(var))
if config[CONF_TYPE] == CONF_NUMBER:
var = await cg.get_variable(config[CONF_NUMBER])
cg.add(item.set_number_variable(var))
cg.add(item.set_format(config[CONF_FORMAT]))
if config[CONF_TYPE] == CONF_SWITCH:
var = await cg.get_variable(config[CONF_SWITCH])
cg.add(item.set_switch_variable(var))
cg.add(item.set_on_text(config[CONF_ON_TEXT]))
cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_NEXT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_PREV, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
async def display_menu_to_code(menu, config):
cg.add(menu.set_active(config[CONF_ACTIVE]))
root_item = cg.new_Pvariable(config[CONF_ROOT_ITEM_ID])
cg.add(menu.set_root_item(root_item))
cg.add(menu.set_mode(config[CONF_MODE]))
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, root_item)
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)

View File

@ -0,0 +1,133 @@
#pragma once
#include "esphome/core/automation.h"
#include "display_menu_base.h"
namespace esphome {
namespace display_menu_base {
template<typename... Ts> class UpAction : public Action<Ts...> {
public:
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->up(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class DownAction : public Action<Ts...> {
public:
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->down(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class LeftAction : public Action<Ts...> {
public:
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->left(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class RightAction : public Action<Ts...> {
public:
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->right(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class EnterAction : public Action<Ts...> {
public:
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->enter(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class ShowAction : public Action<Ts...> {
public:
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class HideAction : public Action<Ts...> {
public:
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->hide(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class ShowMainAction : public Action<Ts...> {
public:
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show_main(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
public:
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
bool check(Ts... x) override { return this->menu_->is_active(); }
protected:
DisplayMenuComponent *menu_;
};
class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
}
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,315 @@
#include "display_menu_base.h"
#include <algorithm>
namespace esphome {
namespace display_menu_base {
void DisplayMenuComponent::up() {
if (this->check_healthy_and_active_()) {
bool changed = false;
if (this->editing_) {
switch (this->mode_) {
case MENU_MODE_ROTARY:
changed = this->get_selected_item_()->select_prev();
break;
default:
break;
}
} else {
changed = this->cursor_up_();
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::down() {
if (this->check_healthy_and_active_()) {
bool changed = false;
if (this->editing_) {
switch (this->mode_) {
case MENU_MODE_ROTARY:
changed = this->get_selected_item_()->select_next();
break;
default:
break;
}
} else {
changed = this->cursor_down_();
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::left() {
if (this->check_healthy_and_active_()) {
bool changed = false;
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_NUMBER:
case MENU_ITEM_CUSTOM:
switch (this->mode_) {
case MENU_MODE_ROTARY:
if (this->editing_) {
this->finish_editing_();
changed = true;
}
break;
case MENU_MODE_JOYSTICK:
if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
changed = this->get_selected_item_()->select_prev();
break;
default:
break;
}
break;
case MENU_ITEM_BACK:
changed = this->leave_menu_();
break;
default:
break;
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::right() {
if (this->check_healthy_and_active_()) {
bool changed = false;
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_NUMBER:
case MENU_ITEM_CUSTOM:
switch (this->mode_) {
case MENU_MODE_JOYSTICK:
if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
changed = this->get_selected_item_()->select_next();
default:
break;
}
break;
case MENU_ITEM_MENU:
changed = this->enter_menu_();
break;
default:
break;
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::enter() {
if (this->check_healthy_and_active_()) {
bool changed = false;
MenuItem *item = this->get_selected_item_();
if (this->editing_) {
this->finish_editing_();
changed = true;
} else {
switch (item->get_type()) {
case MENU_ITEM_MENU:
changed = this->enter_menu_();
break;
case MENU_ITEM_BACK:
changed = this->leave_menu_();
break;
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_CUSTOM:
if (item->get_immediate_edit()) {
changed = item->select_next();
} else {
this->editing_ = true;
item->on_enter();
changed = true;
}
break;
case MENU_ITEM_NUMBER:
// A number cannot be immediate in the rotary mode
if (!item->get_immediate_edit() || this->mode_ == MENU_MODE_ROTARY) {
this->editing_ = true;
item->on_enter();
changed = true;
}
break;
case MENU_ITEM_COMMAND:
changed = item->select_next();
break;
default:
break;
}
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::draw() {
if (this->check_healthy_and_active_())
this->draw_menu();
}
void DisplayMenuComponent::show_main() {
bool disp_changed = false;
if (this->is_failed())
return;
this->process_initial_();
if (this->active_ && this->editing_)
this->finish_editing_();
if (this->displayed_item_ != this->root_item_) {
this->displayed_item_->on_leave();
disp_changed = true;
}
this->reset_();
this->active_ = true;
if (disp_changed) {
this->displayed_item_->on_enter();
}
this->draw_and_update();
}
void DisplayMenuComponent::show() {
if (this->is_failed())
return;
this->process_initial_();
if (!this->active_) {
this->active_ = true;
this->draw_and_update();
}
}
void DisplayMenuComponent::hide() {
if (this->check_healthy_and_active_()) {
if (this->editing_)
this->finish_editing_();
this->active_ = false;
this->update();
}
}
void DisplayMenuComponent::reset_() {
this->displayed_item_ = this->root_item_;
this->cursor_index_ = this->top_index_ = 0;
this->selection_stack_.clear();
}
void DisplayMenuComponent::process_initial_() {
if (!this->root_on_enter_called_) {
this->root_item_->on_enter();
this->root_on_enter_called_ = true;
}
}
bool DisplayMenuComponent::check_healthy_and_active_() {
if (this->is_failed())
return false;
this->process_initial_();
return this->active_;
}
bool DisplayMenuComponent::cursor_up_() {
bool changed = false;
if (this->cursor_index_ > 0) {
changed = true;
--this->cursor_index_;
if (this->cursor_index_ < this->top_index_)
this->top_index_ = this->cursor_index_;
}
return changed;
}
bool DisplayMenuComponent::cursor_down_() {
bool changed = false;
if (this->cursor_index_ + 1 < this->displayed_item_->items_size()) {
changed = true;
++this->cursor_index_;
if (this->cursor_index_ >= this->top_index_ + this->rows_)
this->top_index_ = this->cursor_index_ - this->rows_ + 1;
}
return changed;
}
bool DisplayMenuComponent::enter_menu_() {
this->displayed_item_->on_leave();
this->displayed_item_ = static_cast<MenuItemMenu *>(this->get_selected_item_());
this->selection_stack_.push_front({this->top_index_, this->cursor_index_});
this->cursor_index_ = this->top_index_ = 0;
this->displayed_item_->on_enter();
return true;
}
bool DisplayMenuComponent::leave_menu_() {
bool changed = false;
if (this->displayed_item_->get_parent() != nullptr) {
this->displayed_item_->on_leave();
this->displayed_item_ = this->displayed_item_->get_parent();
this->top_index_ = this->selection_stack_.front().first;
this->cursor_index_ = this->selection_stack_.front().second;
this->selection_stack_.pop_front();
this->displayed_item_->on_enter();
changed = true;
}
return changed;
}
void DisplayMenuComponent::finish_editing_() {
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_NUMBER:
case MENU_ITEM_SWITCH:
case MENU_ITEM_CUSTOM:
this->get_selected_item_()->on_leave();
break;
default:
break;
}
this->editing_ = false;
}
void DisplayMenuComponent::draw_menu() {
for (size_t i = 0; i < this->rows_ && this->top_index_ + i < this->displayed_item_->items_size(); ++i) {
this->draw_item(this->displayed_item_->get_item(this->top_index_ + i), i,
this->top_index_ + i == this->cursor_index_);
}
}
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,77 @@
#pragma once
#include "esphome/core/component.h"
#include "menu_item.h"
#include <forward_list>
namespace esphome {
namespace display_menu_base {
enum MenuMode {
MENU_MODE_ROTARY,
MENU_MODE_JOYSTICK,
};
class MenuItem;
/** Class to display a hierarchical menu.
*
*/
class DisplayMenuComponent : public Component {
public:
void set_root_item(MenuItemMenu *item) { this->displayed_item_ = this->root_item_ = item; }
void set_active(bool active) { this->active_ = active; }
void set_mode(MenuMode mode) { this->mode_ = mode; }
void set_rows(uint8_t rows) { this->rows_ = rows; }
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void up();
void down();
void left();
void right();
void enter();
void show_main();
void show();
void hide();
void draw();
bool is_active() const { return this->active_; }
protected:
void reset_();
void process_initial_();
bool check_healthy_and_active_();
MenuItem *get_selected_item_() { return this->displayed_item_->get_item(this->cursor_index_); }
bool cursor_up_();
bool cursor_down_();
bool enter_menu_();
bool leave_menu_();
void finish_editing_();
virtual void draw_menu();
virtual void draw_item(const MenuItem *item, uint8_t row, bool selected) = 0;
virtual void update() {}
virtual void draw_and_update() {
draw_menu();
update();
}
uint8_t rows_;
bool active_;
MenuMode mode_;
MenuItemMenu *root_item_{nullptr};
MenuItemMenu *displayed_item_{nullptr};
uint8_t top_index_{0};
uint8_t cursor_index_{0};
std::forward_list<std::pair<uint8_t, uint8_t>> selection_stack_{};
bool editing_{false};
bool root_on_enter_called_{false};
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,179 @@
#include "menu_item.h"
#include <cstdio>
namespace esphome {
namespace display_menu_base {
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }
void MenuItem::on_value_() { this->on_value_callbacks_.call(); }
#ifdef USE_SELECT
std::string MenuItemSelect::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
if (this->select_var_ != nullptr) {
result = this->select_var_->state;
}
}
return result;
}
bool MenuItemSelect::select_next() {
bool changed = false;
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
changed = true;
}
return changed;
}
bool MenuItemSelect::select_prev() {
bool changed = false;
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
changed = true;
}
return changed;
}
#endif // USE_SELECT
#ifdef USE_NUMBER
std::string MenuItemNumber::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
char data[32];
snprintf(data, sizeof(data), this->format_.c_str(), get_number_value_());
result = data;
}
return result;
}
bool MenuItemNumber::select_next() {
bool changed = false;
if (this->number_var_ != nullptr) {
float last = this->number_var_->state;
this->number_var_->make_call().number_increment(false).perform();
if (this->number_var_->state != last) {
this->on_value_();
changed = true;
}
}
return changed;
}
bool MenuItemNumber::select_prev() {
bool changed = false;
if (this->number_var_ != nullptr) {
float last = this->number_var_->state;
this->number_var_->make_call().number_decrement(false).perform();
if (this->number_var_->state != last) {
this->on_value_();
changed = true;
}
}
return changed;
}
float MenuItemNumber::get_number_value_() const {
float result = 0.0;
if (this->number_var_ != nullptr) {
if (!this->number_var_->has_state() || this->number_var_->state < this->number_var_->traits.get_min_value()) {
result = this->number_var_->traits.get_min_value();
} else if (this->number_var_->state > this->number_var_->traits.get_max_value()) {
result = this->number_var_->traits.get_max_value();
} else {
result = this->number_var_->state;
}
}
return result;
}
#endif // USE_NUMBER
#ifdef USE_SWITCH
std::string MenuItemSwitch::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
result = this->get_switch_state_() ? this->switch_on_text_ : this->switch_off_text_;
}
return result;
}
bool MenuItemSwitch::select_next() { return this->toggle_switch_(); }
bool MenuItemSwitch::select_prev() { return this->toggle_switch_(); }
bool MenuItemSwitch::get_switch_state_() const { return (this->switch_var_ != nullptr && this->switch_var_->state); }
bool MenuItemSwitch::toggle_switch_() {
bool changed = false;
if (this->switch_var_ != nullptr) {
this->switch_var_->toggle();
this->on_value_();
changed = true;
}
return changed;
}
#endif // USE_SWITCH
std::string MenuItemCustom::get_value_text() const {
return (this->value_getter_.has_value()) ? this->value_getter_.value()(this) : "";
}
bool MenuItemCommand::select_next() {
this->on_value_();
return true;
}
bool MenuItemCommand::select_prev() {
this->on_value_();
return true;
}
bool MenuItemCustom::select_next() {
this->on_next_();
this->on_value_();
return true;
}
bool MenuItemCustom::select_prev() {
this->on_prev_();
this->on_value_();
return true;
}
void MenuItemCustom::on_next_() { this->on_next_callbacks_.call(); }
void MenuItemCustom::on_prev_() { this->on_prev_callbacks_.call(); }
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,187 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <vector>
namespace esphome {
namespace display_menu_base {
enum MenuItemType {
MENU_ITEM_LABEL,
MENU_ITEM_MENU,
MENU_ITEM_BACK,
MENU_ITEM_SELECT,
MENU_ITEM_NUMBER,
MENU_ITEM_SWITCH,
MENU_ITEM_COMMAND,
MENU_ITEM_CUSTOM,
};
class MenuItem;
class MenuItemMenu;
using value_getter_t = std::function<std::string(const MenuItem *)>;
class MenuItem {
public:
explicit MenuItem(MenuItemType t) : item_type_(t) {}
void set_parent(MenuItemMenu *parent) { this->parent_ = parent; }
MenuItemMenu *get_parent() { return this->parent_; }
MenuItemType get_type() const { return this->item_type_; }
template<typename V> void set_text(V val) { this->text_ = val; }
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
virtual bool get_immediate_edit() const { return false; }
virtual bool has_value() const { return false; }
virtual std::string get_value_text() const { return ""; }
virtual bool select_next() { return false; }
virtual bool select_prev() { return false; }
void on_enter();
void on_leave();
protected:
void on_value_();
MenuItemType item_type_;
MenuItemMenu *parent_{nullptr};
TemplatableValue<std::string, const MenuItem *> text_;
CallbackManager<void()> on_enter_callbacks_{};
CallbackManager<void()> on_leave_callbacks_{};
CallbackManager<void()> on_value_callbacks_{};
};
class MenuItemMenu : public MenuItem {
public:
explicit MenuItemMenu() : MenuItem(MENU_ITEM_MENU) {}
void add_item(MenuItem *item) {
item->set_parent(this);
this->items_.push_back(item);
}
size_t items_size() const { return this->items_.size(); }
MenuItem *get_item(size_t i) { return this->items_[i]; }
protected:
std::vector<MenuItem *> items_;
};
class MenuItemEditable : public MenuItem {
public:
explicit MenuItemEditable(MenuItemType t) : MenuItem(t) {}
void set_immediate_edit(bool val) { this->immediate_edit_ = val; }
bool get_immediate_edit() const override { return this->immediate_edit_; }
void set_value_lambda(value_getter_t &&getter) { this->value_getter_ = getter; }
protected:
bool immediate_edit_{false};
optional<value_getter_t> value_getter_{};
};
#ifdef USE_SELECT
class MenuItemSelect : public MenuItemEditable {
public:
explicit MenuItemSelect() : MenuItemEditable(MENU_ITEM_SELECT) {}
void set_select_variable(select::Select *var) { this->select_var_ = var; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
select::Select *select_var_{nullptr};
};
#endif
#ifdef USE_NUMBER
class MenuItemNumber : public MenuItemEditable {
public:
explicit MenuItemNumber() : MenuItemEditable(MENU_ITEM_NUMBER) {}
void set_number_variable(number::Number *var) { this->number_var_ = var; }
void set_format(const std::string &fmt) { this->format_ = fmt; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
float get_number_value_() const;
number::Number *number_var_{nullptr};
std::string format_;
};
#endif
#ifdef USE_SWITCH
class MenuItemSwitch : public MenuItemEditable {
public:
explicit MenuItemSwitch() : MenuItemEditable(MENU_ITEM_SWITCH) {}
void set_switch_variable(switch_::Switch *var) { this->switch_var_ = var; }
void set_on_text(const std::string &t) { this->switch_on_text_ = t; }
void set_off_text(const std::string &t) { this->switch_off_text_ = t; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
bool get_switch_state_() const;
bool toggle_switch_();
switch_::Switch *switch_var_{nullptr};
std::string switch_on_text_;
std::string switch_off_text_;
};
#endif
class MenuItemCommand : public MenuItem {
public:
explicit MenuItemCommand() : MenuItem(MENU_ITEM_COMMAND) {}
bool select_next() override;
bool select_prev() override;
};
class MenuItemCustom : public MenuItemEditable {
public:
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
bool has_value() const override { return this->value_getter_.has_value(); }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
void on_next_();
void on_prev_();
CallbackManager<void()> on_next_callbacks_{};
CallbackManager<void()> on_prev_callbacks_{};
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -105,6 +105,12 @@ _esp32_validations = {
def validate_gpio_pin(value):
value = _translate_pin(value)
board = CORE.data[KEY_ESP32][KEY_BOARD]
board_pins = boards.ESP32_BOARD_PINS.get(board, {})
if value in board_pins.values():
return value
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID
from esphome.core import CORE
from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER
from esphome.core import CORE, TimePeriod
from esphome.components.esp32 import add_idf_sdkconfig_option
DEPENDENCIES = ["esp32"]
@ -12,16 +12,47 @@ ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component)
CONF_MAJOR = "major"
CONF_MINOR = "minor"
CONF_MIN_INTERVAL = "min_interval"
CONF_MAX_INTERVAL = "max_interval"
CONF_MEASURED_POWER = "measured_power"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
cv.Required(CONF_UUID): cv.uuid,
cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
cv.Optional(CONF_MINOR, default=61958): cv.uint16_t,
}
).extend(cv.COMPONENT_SCHEMA)
def validate_config(config):
if config[CONF_MIN_INTERVAL] > config.get(CONF_MAX_INTERVAL):
raise cv.Invalid("min_interval must be <= max_interval")
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
cv.Required(CONF_UUID): cv.uuid,
cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
cv.Optional(CONF_MINOR, default=61958): cv.uint16_t,
cv.Optional(CONF_MIN_INTERVAL, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
),
),
cv.Optional(CONF_MAX_INTERVAL, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
),
),
cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range(
min=-128, max=0
),
cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True)
),
}
).extend(cv.COMPONENT_SCHEMA),
validate_config,
)
async def to_code(config):
@ -31,6 +62,10 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_major(config[CONF_MAJOR]))
cg.add(var.set_minor(config[CONF_MINOR]))
cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL]))
cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)

View File

@ -36,11 +36,24 @@ static esp_ble_adv_params_t ble_adv_params = {
#define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8))
static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = {
.flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502};
.flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}};
void ESP32BLEBeacon::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:");
ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_);
char uuid[37];
char *bpos = uuid;
for (int8_t ii = 0; ii < 16; ++ii) {
bpos += sprintf(bpos, "%02X", this->uuid_[ii]);
if (ii == 3 || ii == 5 || ii == 7 || ii == 9) {
bpos += sprintf(bpos, "-");
}
}
uuid[36] = '\0';
ESP_LOGCONFIG(TAG,
" UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
", TX Power: %ddBm",
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
this->tx_power_);
}
void ESP32BLEBeacon::setup() {
@ -67,6 +80,9 @@ void ESP32BLEBeacon::ble_core_task(void *params) {
}
void ESP32BLEBeacon::ble_setup() {
ble_adv_params.adv_int_min = static_cast<uint16_t>(global_esp32_ble_beacon->min_interval_ / 0.625f);
ble_adv_params.adv_int_max = static_cast<uint16_t>(global_esp32_ble_beacon->max_interval_ / 0.625f);
// Initialize non-volatile storage for the bluetooth controller
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
@ -118,6 +134,12 @@ void ESP32BLEBeacon::ble_setup() {
ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
return;
}
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV,
static_cast<esp_power_level_t>((global_esp32_ble_beacon->tx_power_ + 12) / 3));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
return;
}
err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
@ -130,7 +152,7 @@ void ESP32BLEBeacon::ble_setup() {
sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid));
ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_);
ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_);
ibeacon_adv_data.ibeacon_vendor.measured_power = 0xC5;
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(global_esp32_ble_beacon->measured_power_);
esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
}
@ -153,7 +175,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
break;
}
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: {
err = param->adv_start_cmpl.status;
err = param->adv_stop_cmpl.status;
if (err != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "BLE adv stop failed: %s", esp_err_to_name(err));
} else {

View File

@ -5,6 +5,7 @@
#ifdef USE_ESP32
#include <esp_gap_ble_api.h>
#include <esp_bt.h>
namespace esphome {
namespace esp32_ble_beacon {
@ -14,8 +15,8 @@ typedef struct {
uint8_t flags[3];
uint8_t length;
uint8_t type;
uint16_t company_id;
uint16_t beacon_type;
uint8_t company_id[2];
uint8_t beacon_type[2];
} __attribute__((packed)) esp_ble_ibeacon_head_t;
// NOLINTNEXTLINE(modernize-use-using)
@ -42,6 +43,10 @@ class ESP32BLEBeacon : public Component {
void set_major(uint16_t major) { this->major_ = major; }
void set_minor(uint16_t minor) { this->minor_ = minor; }
void set_min_interval(uint16_t val) { this->min_interval_ = val; }
void set_max_interval(uint16_t val) { this->max_interval_ = val; }
void set_measured_power(int8_t val) { this->measured_power_ = val; }
void set_tx_power(int8_t val) { this->tx_power_ = val; }
protected:
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
@ -51,6 +56,10 @@ class ESP32BLEBeacon : public Component {
std::array<uint8_t, 16> uuid_;
uint16_t major_{};
uint16_t minor_{};
uint16_t min_interval_{};
uint16_t max_interval_{};
int8_t measured_power_{};
int8_t tx_power_{};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -9,7 +9,7 @@
namespace esphome {
namespace esp32_ble_client {
static const char *const TAG = "esp32_ble_client.characteristic";
static const char *const TAG = "esp32_ble_client";
BLECharacteristic::~BLECharacteristic() {
for (auto &desc : this->descriptors)
@ -29,7 +29,8 @@ void BLECharacteristic::parse_descriptors() {
break;
}
if (status != ESP_GATT_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d",
this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
break;
}
if (count == 0) {
@ -41,7 +42,8 @@ void BLECharacteristic::parse_descriptors() {
desc->handle = result.handle;
desc->characteristic = this;
this->descriptors.push_back(desc);
ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle);
offset++;
}
}
@ -69,7 +71,8 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size,
auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size,
new_val, write_type, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status);
ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d",
this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
}
return status;
}

View File

@ -11,6 +11,9 @@ namespace esp32_ble_client {
static const char *const TAG = "esp32_ble_client";
void BLEClientBase::setup() {
static uint8_t connection_index = 0;
this->connection_index_ = connection_index++;
auto ret = esp_ble_gattc_app_register(this->app_id);
if (ret) {
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
@ -33,7 +36,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
return false;
ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str());
ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str());
this->set_state(espbt::ClientState::DISCOVERED);
auto addr = device.address_uint64();
@ -47,80 +50,88 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return true;
}
std::string BLEClientBase::address_str() const {
return str_snprintf("%02x:%02x:%02x:%02x:%02x:%02x", 17, (uint8_t)(this->address_ >> 40) & 0xff,
(uint8_t)(this->address_ >> 32) & 0xff, (uint8_t)(this->address_ >> 24) & 0xff,
(uint8_t)(this->address_ >> 16) & 0xff, (uint8_t)(this->address_ >> 8) & 0xff,
(uint8_t)(this->address_ >> 0) & 0xff);
}
void BLEClientBase::connect() {
ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str());
ESP_LOGI(TAG, "[%d] [%s] Attempting BLE connection", this->connection_index_, this->address_str_.c_str());
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
if (ret) {
ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}
void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
void BLEClientBase::disconnect() {
ESP_LOGI(TAG, "[%d] [%s] Disconnecting.", this->connection_index_, this->address_str_.c_str());
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
err);
}
if (this->state_ == espbt::ClientState::SEARCHING) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
}
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
return;
return false;
if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
return;
return false;
ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_,
this->address_str_.c_str(), event, esp_gattc_if);
switch (event) {
case ESP_GATTC_REG_EVT: {
if (param->reg.status == ESP_GATT_OK) {
ESP_LOGV(TAG, "gattc registered app id %d", this->app_id);
ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(),
this->app_id);
this->gattc_if_ = esp_gattc_if;
} else {
ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status);
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
}
break;
}
case ESP_GATTC_OPEN_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str());
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str());
this->conn_id_ = param->open.conn_id;
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status);
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
param->open.status);
this->set_state(espbt::ClientState::IDLE);
break;
}
break;
}
case ESP_GATTC_CONNECT_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str());
if (this->conn_id_ != param->connect.conn_id) {
ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d",
this->address_str().c_str(), param->connect.conn_id, this->conn_id_);
}
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
if (ret) {
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
break;
}
case ESP_GATTC_CFG_MTU_EVT: {
if (param->cfg_mtu.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu,
param->cfg_mtu.status);
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
this->set_state(espbt::ClientState::IDLE);
break;
}
ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu);
ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
param->cfg_mtu.status, param->cfg_mtu.mtu);
this->mtu_ = param->cfg_mtu.mtu;
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) {
return;
}
ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason);
if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0)
return false;
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason);
for (auto &svc : this->services_)
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.clear();
@ -137,10 +148,12 @@ void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str());
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str());
for (auto &svc : this->services_) {
ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str());
ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle);
ESP_LOGI(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
svc->uuid.to_string().c_str());
ESP_LOGI(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
svc->parse_characteristics();
}
this->set_state(espbt::ClientState::CONNECTED);
@ -149,14 +162,10 @@ void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
auto *descr = this->get_config_descriptor(param->reg_for_notify.handle);
if (descr == nullptr) {
ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle);
break;
}
if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle,
descr->uuid.to_string().c_str());
ESP_LOGW(TAG, "[%d] [%s] Handle 0x%x (uuid %s) is not a client config char uuid", this->connection_index_,
this->address_str_.c_str(), param->reg_for_notify.handle, descr->uuid.to_string().c_str());
break;
}
uint16_t notify_en = 1;
@ -164,7 +173,8 @@ void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, descr->handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
this->address_str_.c_str(), status);
}
break;
}
@ -172,24 +182,28 @@ void BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
default:
break;
}
return true;
}
void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
// This event is sent by the server when it requests security
case ESP_GAP_BLE_SEC_REQ_EVT:
ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event);
ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
// This event is sent once authentication has completed
case ESP_GAP_BLE_AUTH_CMPL_EVT:
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str());
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
format_hex(bd_addr, 6).c_str());
if (!param->ble_security.auth_cmpl.success) {
ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason);
ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(),
param->ble_security.auth_cmpl.fail_reason);
} else {
ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type,
ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
param->ble_security.auth_cmpl.auth_mode);
}
break;
@ -245,7 +259,8 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
(int32_t)(value[4]));
}
}
ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length);
ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_,
this->address_str_.c_str(), value[0], length);
return NAN;
}

View File

@ -28,13 +28,27 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
bool parse_device(const espbt::ESPBTDevice &device) override;
void on_scan_end() override {}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void connect() override;
void disconnect();
void set_address(uint64_t address) { this->address_ = address; }
std::string address_str() const;
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
void set_address(uint64_t address) {
this->address_ = address;
if (address == 0) {
memset(this->remote_bda_, 0, sizeof(this->remote_bda_));
this->address_str_ = "";
} else {
this->address_str_ = str_snprintf("%02x:%02x:%02x:%02x:%02x:%02x", 17, (uint8_t)(this->address_ >> 40) & 0xff,
(uint8_t)(this->address_ >> 32) & 0xff, (uint8_t)(this->address_ >> 24) & 0xff,
(uint8_t)(this->address_ >> 16) & 0xff, (uint8_t)(this->address_ >> 8) & 0xff,
(uint8_t)(this->address_ >> 0) & 0xff);
}
}
std::string address_str() const { return this->address_str_; }
BLEService *get_service(espbt::ESPBTUUID uuid);
BLEService *get_service(uint16_t uuid);
@ -55,12 +69,16 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
uint16_t get_conn_id() const { return this->conn_id_; }
uint64_t get_address() const { return this->address_; }
uint8_t get_connection_index() const { return this->connection_index_; }
protected:
int gattc_if_;
esp_bd_addr_t remote_bda_;
esp_ble_addr_type_t remote_addr_type_;
uint16_t conn_id_;
uint64_t address_;
uint16_t conn_id_{0xFFFF};
uint64_t address_{0};
std::string address_str_{};
uint8_t connection_index_;
uint16_t mtu_{23};
std::vector<BLEService *> services_;

View File

@ -8,7 +8,7 @@
namespace esphome {
namespace esp32_ble_client {
static const char *const TAG = "esp32_ble_client.service";
static const char *const TAG = "esp32_ble_client";
BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) {
for (auto &chr : this->characteristics) {
@ -40,7 +40,8 @@ void BLEService::parse_characteristics() {
break;
}
if (status != ESP_GATT_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(),
this->client->address_str().c_str(), status);
break;
}
if (count == 0) {
@ -53,8 +54,9 @@ void BLEService::parse_characteristics() {
characteristic->handle = result.char_handle;
characteristic->service = this;
this->characteristics.push_back(characteristic);
ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(),
characteristic->handle, characteristic->properties);
ESP_LOGI(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
characteristic->properties);
characteristic->parse_descriptors();
offset++;
}

View File

@ -159,7 +159,7 @@ enum class ClientState {
class ESPBTClient : public ESPBTDeviceListener {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) = 0;
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;

View File

@ -56,7 +56,7 @@ async def to_code(config):
cg.add(ble_server.register_service_component(var))
cg.add_define("USE_IMPROV")
cg.add_library("esphome/Improv", "1.2.1")
cg.add_library("esphome/Improv", "1.2.3")
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))

View File

@ -0,0 +1 @@
CODEOWNERS = ["@gtjadsonsantos"]

View File

@ -0,0 +1,16 @@
#include "ethernet_info_text_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
namespace esphome {
namespace ethernet_info {
static const char *const TAG = "ethernet_info";
void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); }
} // namespace ethernet_info
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/ethernet/ethernet_component.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
namespace esphome {
namespace ethernet_info {
class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
public:
void update() override {
tcpip_adapter_ip_info_t tcpip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &tcpip);
auto ip = tcpip.ip.addr;
if (ip != this->last_ip_) {
this->last_ip_ = ip;
this->publish_state(network::IPAddress(ip).str());
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; }
void dump_config() override;
protected:
network::IPAddress last_ip_;
};
} // namespace ethernet_info
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@ -0,0 +1,34 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import (
CONF_IP_ADDRESS,
ENTITY_CATEGORY_DIAGNOSTIC,
)
DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
IPAddressEsthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s"))
}
)
async def setup_conf(config, key):
if key in config:
conf = config[key]
var = await text_sensor.new_text_sensor(conf)
await cg.register_component(var, conf)
async def to_code(config):
await setup_conf(config, CONF_IP_ADDRESS)

View File

@ -0,0 +1,53 @@
#pragma once
#include <utility>
#include "esphome/core/automation.h"
#include "ezo.h"
namespace esphome {
namespace ezo {
class LedTrigger : public Trigger<bool> {
public:
explicit LedTrigger(EZOSensor *ezo) {
ezo->add_led_state_callback([this](bool value) { this->trigger(value); });
}
};
class CustomTrigger : public Trigger<std::string> {
public:
explicit CustomTrigger(EZOSensor *ezo) {
ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class TTrigger : public Trigger<std::string> {
public:
explicit TTrigger(EZOSensor *ezo) {
ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class CalibrationTrigger : public Trigger<std::string> {
public:
explicit CalibrationTrigger(EZOSensor *ezo) {
ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class SlopeTrigger : public Trigger<std::string> {
public:
explicit SlopeTrigger(EZOSensor *ezo) {
ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
class DeviceInformationTrigger : public Trigger<std::string> {
public:
explicit DeviceInformationTrigger(EZOSensor *ezo) {
ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); });
}
};
} // namespace ezo
} // namespace esphome

View File

@ -5,11 +5,11 @@
namespace esphome {
namespace ezo {
static const char *const TAG = "ezo.sensor";
static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION",
"EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP",
"EZO_I2C", "EZO_T", "EZO_CUSTOM"};
static const uint16_t EZO_STATE_WAIT = 1;
static const uint16_t EZO_STATE_SEND_TEMP = 2;
static const uint16_t EZO_STATE_WAIT_TEMP = 4;
static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"};
void EZOSensor::dump_config() {
LOG_SENSOR("", "EZO", this);
@ -20,37 +20,75 @@ void EZOSensor::dump_config() {
}
void EZOSensor::update() {
if (this->state_ & EZO_STATE_WAIT) {
ESP_LOGE(TAG, "update overrun, still waiting for previous response");
// Check if a read is in there already and if not insert on in the second position
if (!this->commands_.empty() && this->commands_.front()->command_type != EzoCommandType::EZO_READ &&
this->commands_.size() > 1) {
bool found = false;
for (auto &i : this->commands_) {
if (i->command_type == EzoCommandType::EZO_READ) {
found = true;
break;
}
}
if (!found) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = "R";
ezo_command->command_type = EzoCommandType::EZO_READ;
ezo_command->delay_ms = 900;
auto it = this->commands_.begin();
++it;
this->commands_.insert(it, std::move(ezo_command));
}
return;
}
uint8_t c = 'R';
this->write(&c, 1);
this->state_ |= EZO_STATE_WAIT;
this->start_time_ = millis();
this->wait_time_ = 900;
this->get_state();
}
void EZOSensor::loop() {
uint8_t buf[21];
if (!(this->state_ & EZO_STATE_WAIT)) {
if (this->state_ & EZO_STATE_SEND_TEMP) {
int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_);
this->write(buf, len);
this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP;
this->start_time_ = millis();
this->wait_time_ = 300;
if (this->commands_.empty()) {
return;
}
EzoCommand *to_run = this->commands_.front().get();
if (!to_run->command_sent) {
const uint8_t *data = reinterpret_cast<const uint8_t *>(to_run->command.c_str());
ESP_LOGVV(TAG, "Sending command \"%s\"", data);
this->write(data, to_run->command.length());
if (to_run->command_type == EzoCommandType::EZO_SLEEP ||
to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data
this->commands_.pop_front();
if (to_run->command_type == EzoCommandType::EZO_I2C)
this->address_ = this->new_address_;
return;
}
this->start_time_ = millis();
to_run->command_sent = true;
return;
}
if (millis() - this->start_time_ < this->wait_time_)
if (millis() - this->start_time_ < to_run->delay_ms)
return;
uint8_t buf[32];
buf[0] = 0;
if (!this->read_bytes_raw(buf, 20)) {
if (!this->read_bytes_raw(buf, 32)) {
ESP_LOGE(TAG, "read error");
this->state_ = 0;
this->commands_.pop_front();
return;
}
switch (buf[0]) {
case 1:
break;
@ -66,28 +104,142 @@ void EZOSensor::loop() {
ESP_LOGE(TAG, "device returned an unknown response: %d", buf[0]);
break;
}
if (this->state_ & EZO_STATE_WAIT_TEMP) {
this->state_ = 0;
return;
}
this->state_ &= ~EZO_STATE_WAIT;
if (buf[0] != 1)
return;
// some sensors return multiple comma-separated values, terminate string after first one
for (size_t i = 1; i < sizeof(buf) - 1; i++) {
if (buf[i] == ',')
buf[i] = '\0';
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", buf, EZO_COMMAND_TYPE_STRINGS[to_run->command_type]);
if ((buf[0] == 1) || (to_run->command_type == EzoCommandType::EZO_CALIBRATION)) { // EZO_CALIBRATION returns 0-3
// some sensors return multiple comma-separated values, terminate string after first one
for (size_t i = 1; i < sizeof(buf) - 1; i++) {
if (buf[i] == ',') {
buf[i] = '\0';
break;
}
}
std::string payload = reinterpret_cast<char *>(&buf[1]);
if (!payload.empty()) {
switch (to_run->command_type) {
case EzoCommandType::EZO_READ: {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
} else {
this->publish_state(*val);
}
break;
}
case EzoCommandType::EZO_LED: {
this->led_callback_.call(payload.back() == '1');
break;
}
case EzoCommandType::EZO_DEVICE_INFORMATION: {
int start_location = 0;
if ((start_location = payload.find(',')) != std::string::npos) {
this->device_infomation_callback_.call(payload.substr(start_location + 1));
}
break;
}
case EzoCommandType::EZO_SLOPE: {
int start_location = 0;
if ((start_location = payload.find(',')) != std::string::npos) {
this->slope_callback_.call(payload.substr(start_location + 1));
}
break;
}
case EzoCommandType::EZO_CALIBRATION: {
int start_location = 0;
if ((start_location = payload.find(',')) != std::string::npos) {
this->calibration_callback_.call(payload.substr(start_location + 1));
}
break;
}
case EzoCommandType::EZO_T: {
this->t_callback_.call(payload);
break;
}
case EzoCommandType::EZO_CUSTOM: {
this->custom_callback_.call(payload);
break;
}
default: {
break;
}
}
}
}
float val = parse_number<float>((char *) &buf[1]).value_or(0);
this->publish_state(val);
this->commands_.pop_front();
}
void EZOSensor::set_tempcomp_value(float temp) {
this->tempcomp_ = temp;
this->state_ |= EZO_STATE_SEND_TEMP;
void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command;
ezo_command->command_type = command_type;
ezo_command->delay_ms = delay_ms;
this->commands_.push_back(std::move(ezo_command));
};
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
std::string payload = str_sprintf("I2C,%u", address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
ESP_LOGE(TAG, "Invalid I2C address");
}
}
void EZOSensor::get_device_information() { this->add_command_("i", EzoCommandType::EZO_DEVICE_INFORMATION); }
void EZOSensor::set_sleep() { this->add_command_("Sleep", EzoCommandType::EZO_SLEEP); }
void EZOSensor::get_state() { this->add_command_("R", EzoCommandType::EZO_READ, 900); }
void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_SLOPE); }
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
std::string payload = str_sprintf("T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
void EZOSensor::set_tempcomp_value(float temp) { this->set_t(temp); }
void EZOSensor::get_calibration() { this->add_command_("Cal,?", EzoCommandType::EZO_CALIBRATION); }
void EZOSensor::set_calibration_point_low(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_LOW, value);
}
void EZOSensor::set_calibration_point_mid(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_MID, value);
}
void EZOSensor::set_calibration_point_high(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_HIGH, value);
}
void EZOSensor::set_calibration_generic(float value) {
std::string payload = str_sprintf("Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommandType::EZO_CALIBRATION); }
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) {
std::string to_send = "L,";
to_send += on ? "1" : "0";
this->add_command_(to_send, EzoCommandType::EZO_LED);
}
void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); }
} // namespace ezo
} // namespace esphome

View File

@ -3,10 +3,35 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include <deque>
namespace esphome {
namespace ezo {
static const char *const TAG = "ezo.sensor";
enum EzoCommandType : uint8_t {
EZO_READ = 0,
EZO_LED = 1,
EZO_DEVICE_INFORMATION = 2,
EZO_SLOPE = 3,
EZO_CALIBRATION,
EZO_SLEEP = 4,
EZO_I2C = 5,
EZO_T = 6,
EZO_CUSTOM = 7
};
enum EzoCalibrationType : uint8_t { EZO_CAL_LOW = 0, EZO_CAL_MID = 1, EZO_CAL_HIGH = 2 };
class EzoCommand {
public:
std::string command;
uint16_t delay_ms = 0;
bool command_sent = false;
EzoCommandType command_type;
};
/// This class implements support for the EZO circuits in i2c mode
class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
@ -15,13 +40,71 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
void update() override;
float get_setup_priority() const override { return setup_priority::DATA; };
void set_tempcomp_value(float temp);
// I2C
void set_address(uint8_t address);
// Device Information
void get_device_information();
void add_device_infomation_callback(std::function<void(std::string)> &&callback) {
this->device_infomation_callback_.add(std::move(callback));
}
// Sleep
void set_sleep();
// R
void get_state();
// Slope
void get_slope();
void add_slope_callback(std::function<void(std::string)> &&callback) {
this->slope_callback_.add(std::move(callback));
}
// T
void get_t();
void set_t(float value);
void set_tempcomp_value(float temp); // For backwards compatibility
void add_t_callback(std::function<void(std::string)> &&callback) { this->t_callback_.add(std::move(callback)); }
// Calibration
void get_calibration();
void set_calibration_point_low(float value);
void set_calibration_point_mid(float value);
void set_calibration_point_high(float value);
void set_calibration_generic(float value);
void clear_calibration();
void add_calibration_callback(std::function<void(std::string)> &&callback) {
this->calibration_callback_.add(std::move(callback));
}
// LED
void get_led_state();
void set_led_state(bool on);
void add_led_state_callback(std::function<void(bool)> &&callback) { this->led_callback_.add(std::move(callback)); }
// Custom
void send_custom(const std::string &to_send);
void add_custom_callback(std::function<void(std::string)> &&callback) {
this->custom_callback_.add(std::move(callback));
}
protected:
std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_;
void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value);
CallbackManager<void(std::string)> device_infomation_callback_{};
CallbackManager<void(std::string)> calibration_callback_{};
CallbackManager<void(std::string)> slope_callback_{};
CallbackManager<void(std::string)> t_callback_{};
CallbackManager<void(std::string)> custom_callback_{};
CallbackManager<void(bool)> led_callback_{};
uint32_t start_time_ = 0;
uint32_t wait_time_ = 0;
uint16_t state_ = 0;
float tempcomp_;
};
} // namespace ezo

View File

@ -1,22 +1,81 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import i2c, sensor
from esphome.const import CONF_ID
from esphome.const import CONF_ID, CONF_TRIGGER_ID
CODEOWNERS = ["@ssieb"]
DEPENDENCIES = ["i2c"]
CONF_ON_LED = "on_led"
CONF_ON_DEVICE_INFORMATION = "on_device_information"
CONF_ON_SLOPE = "on_slope"
CONF_ON_CALIBRATION = "on_calibration"
CONF_ON_T = "on_t"
CONF_ON_CUSTOM = "on_custom"
ezo_ns = cg.esphome_ns.namespace("ezo")
EZOSensor = ezo_ns.class_(
"EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
CustomTrigger = ezo_ns.class_(
"CustomTrigger", automation.Trigger.template(cg.std_string)
)
TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string))
SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string))
CalibrationTrigger = ezo_ns.class_(
"CalibrationTrigger", automation.Trigger.template(cg.std_string)
)
DeviceInformationTrigger = ezo_ns.class_(
"DeviceInformationTrigger", automation.Trigger.template(cg.std_string)
)
LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_))
CONFIG_SCHEMA = (
sensor.SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EZOSensor),
cv.Optional(CONF_ON_CUSTOM): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger),
}
),
cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger),
}
),
cv.Optional(CONF_ON_SLOPE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger),
}
),
cv.Optional(CONF_ON_T): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger),
}
),
cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DeviceInformationTrigger
),
}
),
cv.Optional(CONF_ON_LED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
@ -29,3 +88,27 @@ async def to_code(config):
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)
for conf in config.get(CONF_ON_CUSTOM, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
for conf in config.get(CONF_ON_LED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
for conf in config.get(CONF_ON_DEVICE_INFORMATION, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
for conf in config.get(CONF_ON_SLOPE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
for conf in config.get(CONF_ON_CALIBRATION, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
for conf in config.get(CONF_ON_T, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)

View File

@ -0,0 +1,291 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION
from esphome import automation
from esphome.automation import maybe_simple_id
CODEOWNERS = ["@carlos-sarmiento"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_VOLUME = "volume"
CONF_VOLUME_PER_MINUTE = "volume_per_minute"
ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp")
EzoPMP = ezo_pmp_ns.class_("EzoPMP", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EzoPMP),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(103))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
EZO_PMP_NO_ARGS_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
}
)
# Actions that do not require more arguments
EzoPMPFindAction = ezo_pmp_ns.class_("EzoPMPFindAction", automation.Action)
EzoPMPClearTotalVolumeDispensedAction = ezo_pmp_ns.class_(
"EzoPMPClearTotalVolumeDispensedAction", automation.Action
)
EzoPMPClearCalibrationAction = ezo_pmp_ns.class_(
"EzoPMPClearCalibrationAction", automation.Action
)
EzoPMPPauseDosingAction = ezo_pmp_ns.class_(
"EzoPMPPauseDosingAction", automation.Action
)
EzoPMPStopDosingAction = ezo_pmp_ns.class_("EzoPMPStopDosingAction", automation.Action)
EzoPMPDoseContinuouslyAction = ezo_pmp_ns.class_(
"EzoPMPDoseContinuouslyAction", automation.Action
)
# Actions that require more arguments
EzoPMPDoseVolumeAction = ezo_pmp_ns.class_("EzoPMPDoseVolumeAction", automation.Action)
EzoPMPDoseVolumeOverTimeAction = ezo_pmp_ns.class_(
"EzoPMPDoseVolumeOverTimeAction", automation.Action
)
EzoPMPDoseWithConstantFlowRateAction = ezo_pmp_ns.class_(
"EzoPMPDoseWithConstantFlowRateAction", automation.Action
)
EzoPMPSetCalibrationVolumeAction = ezo_pmp_ns.class_(
"EzoPMPSetCalibrationVolumeAction", automation.Action
)
EzoPMPChangeI2CAddressAction = ezo_pmp_ns.class_(
"EzoPMPChangeI2CAddressAction", automation.Action
)
EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_(
"EzoPMPArbitraryCommandAction", automation.Action
)
@automation.register_action(
"ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
)
async def ezo_pmp_find_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"ezo_pmp.dose_continuously",
EzoPMPDoseContinuouslyAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
)
async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"ezo_pmp.clear_total_volume_dosed",
EzoPMPClearTotalVolumeDispensedAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
)
async def ezo_pmp_clear_total_volume_dosed_to_code(
config, action_id, template_arg, args
):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"ezo_pmp.clear_calibration",
EzoPMPClearCalibrationAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
)
async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
)
async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
)
async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
# Actions that require Multiple Args
EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_VOLUME): cv.templatable(
cv.float_range()
), # Any way to represent as proper volume (vs. raw int)
}
)
@automation.register_action(
"ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA
)
async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double)
cg.add(var.set_volume(template_))
return var
EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_VOLUME): cv.templatable(
cv.float_range()
), # Any way to represent as proper volume (vs. raw int)
cv.Required(CONF_DURATION): cv.templatable(
cv.int_range(1)
), # Any way to represent it as minutes (vs. raw int)
}
)
@automation.register_action(
"ezo_pmp.dose_volume_over_time",
EzoPMPDoseVolumeOverTimeAction,
EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA,
)
async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double)
cg.add(var.set_volume(template_))
template_ = await cg.templatable(config[CONF_DURATION], args, int)
cg.add(var.set_duration(template_))
return var
EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_VOLUME_PER_MINUTE): cv.templatable(
cv.float_range()
), # Any way to represent as proper volume (vs. raw int)
cv.Required(CONF_DURATION): cv.templatable(
cv.int_range(1)
), # Any way to represent it as minutes (vs. raw int)
}
)
@automation.register_action(
"ezo_pmp.dose_with_constant_flow_rate",
EzoPMPDoseWithConstantFlowRateAction,
EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA,
)
async def ezo_pmp_dose_with_constant_flow_rate_to_code(
config, action_id, template_arg, args
):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VOLUME_PER_MINUTE], args, cg.double)
cg.add(var.set_volume(template_))
template_ = await cg.templatable(config[CONF_DURATION], args, int)
cg.add(var.set_duration(template_))
return var
EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_VOLUME): cv.templatable(
cv.float_range()
), # Any way to represent as proper volume (vs. raw int)
}
)
@automation.register_action(
"ezo_pmp.set_calibration_volume",
EzoPMPSetCalibrationVolumeAction,
EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA,
)
async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double)
cg.add(var.set_volume(template_))
return var
EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_ADDRESS): cv.templatable(cv.int_range(min=1, max=127)),
}
)
@automation.register_action(
"ezo_pmp.change_i2c_address",
EzoPMPChangeI2CAddressAction,
EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA,
)
async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double)
cg.add(var.set_address(template_))
return var
EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(EzoPMP),
cv.Required(CONF_COMMAND): cv.templatable(cv.string_strict),
}
)
@automation.register_action(
"ezo_pmp.arbitrary_command",
EzoPMPArbitraryCommandAction,
EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA,
)
async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string)
cg.add(var.set_command(template_))
return var

View File

@ -0,0 +1,42 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
ENTITY_CATEGORY_NONE,
DEVICE_CLASS_RUNNING,
DEVICE_CLASS_EMPTY,
CONF_ID,
)
from . import EzoPMP
DEPENDENCIES = ["ezo_pmp"]
CONF_PUMP_STATE = "pump_state"
CONF_IS_PAUSED = "is_paused"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(EzoPMP),
cv.Optional(CONF_PUMP_STATE): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_RUNNING,
entity_category=ENTITY_CATEGORY_NONE,
),
cv.Optional(CONF_IS_PAUSED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_EMPTY,
entity_category=ENTITY_CATEGORY_NONE,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if CONF_PUMP_STATE in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_PUMP_STATE])
cg.add(parent.set_is_dosing(sens))
if CONF_IS_PAUSED in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_IS_PAUSED])
cg.add(parent.set_is_paused(sens))

View File

@ -0,0 +1,542 @@
#include "ezo_pmp.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace ezo_pmp {
static const char *const TAG = "ezo-pmp";
static const uint16_t EZO_PMP_COMMAND_NONE = 0;
static const uint16_t EZO_PMP_COMMAND_TYPE_READ = 1;
static const uint16_t EZO_PMP_COMMAND_FIND = 2;
static const uint16_t EZO_PMP_COMMAND_DOSE_CONTINUOUSLY = 4;
static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME = 8;
static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME = 16;
static const uint16_t EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE = 32;
static const uint16_t EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME = 64;
static const uint16_t EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED = 128;
static const uint16_t EZO_PMP_COMMAND_CLEAR_CALIBRATION = 256;
static const uint16_t EZO_PMP_COMMAND_PAUSE_DOSING = 512;
static const uint16_t EZO_PMP_COMMAND_STOP_DOSING = 1024;
static const uint16_t EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS = 2048;
static const uint16_t EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS = 4096;
static const uint16_t EZO_PMP_COMMAND_READ_DOSING = 3;
static const uint16_t EZO_PMP_COMMAND_READ_SINGLE_REPORT = 5;
static const uint16_t EZO_PMP_COMMAND_READ_MAX_FLOW_RATE = 9;
static const uint16_t EZO_PMP_COMMAND_READ_PAUSE_STATUS = 17;
static const uint16_t EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED = 33;
static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65;
static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129;
static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257;
static const std::string DOSING_MODE_NONE = "None";
static const std::string DOSING_MODE_VOLUME = "Volume";
static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time";
static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate";
static const std::string DOSING_MODE_CONTINUOUS = "Continuous";
void EzoPMP::dump_config() {
LOG_I2C_DEVICE(this);
if (this->is_failed())
ESP_LOGE(TAG, "Communication with EZO-PMP circuit failed!");
LOG_UPDATE_INTERVAL(this);
}
void EzoPMP::update() {
if (this->is_waiting_) {
return;
}
if (this->is_first_read_) {
this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_);
this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0,
(bool) this->absolute_total_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true);
this->is_first_read_ = false;
}
if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) {
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
if (this->is_dosing_flag_) {
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0,
(bool) this->absolute_total_volume_dosed_);
}
this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_);
} else {
ESP_LOGV(TAG, "Not Scheduling new Command during update()");
}
}
void EzoPMP::loop() {
// If we are not waiting for anything and there is no command to be sent, return
if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) {
return;
}
// If we are not waiting for anything and there IS a command to be sent, do it.
if (!this->is_waiting_ && this->peek_next_command_() != EZO_PMP_COMMAND_NONE) {
this->send_next_command_();
}
// If we are waiting for something but it isn't ready yet, then return
if (this->is_waiting_ && millis() - this->start_time_ < this->wait_time_) {
return;
}
// We are waiting for something and it should be ready.
this->read_command_result_();
}
void EzoPMP::clear_current_command_() {
this->current_command_ = EZO_PMP_COMMAND_NONE;
this->is_waiting_ = false;
}
void EzoPMP::read_command_result_() {
uint8_t response_buffer[21] = {'\0'};
response_buffer[0] = 0;
if (!this->read_bytes_raw(response_buffer, 20)) {
ESP_LOGE(TAG, "read error");
this->clear_current_command_();
return;
}
switch (response_buffer[0]) {
case 254:
return; // keep waiting
case 1:
break;
case 2:
ESP_LOGE(TAG, "device returned a syntax error");
this->clear_current_command_();
return;
case 255:
ESP_LOGE(TAG, "device returned no data");
this->clear_current_command_();
return;
default:
ESP_LOGE(TAG, "device returned an unknown response: %d", response_buffer[0]);
this->clear_current_command_();
return;
}
char first_parameter_buffer[10] = {'\0'};
char second_parameter_buffer[10] = {'\0'};
char third_parameter_buffer[10] = {'\0'};
first_parameter_buffer[0] = '\0';
second_parameter_buffer[0] = '\0';
third_parameter_buffer[0] = '\0';
int current_parameter = 1;
size_t position_in_parameter_buffer = 0;
// some sensors return multiple comma-separated values, terminate string after first one
for (size_t i = 1; i < sizeof(response_buffer) - 1; i++) {
char current_char = response_buffer[i];
if (current_char == '\0') {
ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer);
ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer);
ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer);
ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer);
break;
}
if (current_char == ',') {
current_parameter++;
position_in_parameter_buffer = 0;
continue;
}
switch (current_parameter) {
case 1:
first_parameter_buffer[position_in_parameter_buffer] = current_char;
first_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
break;
case 2:
second_parameter_buffer[position_in_parameter_buffer] = current_char;
second_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
break;
case 3:
third_parameter_buffer[position_in_parameter_buffer] = current_char;
third_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
break;
}
position_in_parameter_buffer++;
}
auto parsed_first_parameter = parse_number<float>(first_parameter_buffer);
auto parsed_second_parameter = parse_number<float>(second_parameter_buffer);
auto parsed_third_parameter = parse_number<float>(third_parameter_buffer);
switch (this->current_command_) {
// Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54
if (parsed_third_parameter.has_value())
this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1;
if (this->is_dosing_)
this->is_dosing_->publish_state(this->is_dosing_flag_);
if (parsed_second_parameter.has_value() && this->last_volume_requested_) {
this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0));
}
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
// If pump is not paused and not dispensing
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
}
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) {
this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0));
}
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57)
if (parsed_second_parameter.has_value() && this->max_flow_rate_)
this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0));
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61)
if (parsed_second_parameter.has_value())
this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1;
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
if (parsed_second_parameter.has_value() && this->total_volume_dosed_)
this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_)
this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65)
if (parsed_second_parameter.has_value() && this->calibration_status_) {
if (parsed_second_parameter.value_or(0) == 1) {
this->calibration_status_->publish_state("Fixed Volume");
} else if (parsed_second_parameter.value_or(0) == 2) {
this->calibration_status_->publish_state("Volume/Time");
} else if (parsed_second_parameter.value_or(0) == 3) {
this->calibration_status_->publish_state("Fixed Volume & Volume/Time");
} else {
this->calibration_status_->publish_state("Uncalibrated");
}
}
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67)
if (parsed_second_parameter.has_value() && this->pump_voltage_)
this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0));
break;
// Non-Read Commands
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
this->is_paused_flag_ = false;
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS:
ESP_LOGI(TAG, "Arbitrary Command Response: %s", (char *) response_buffer);
break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
case EZO_PMP_COMMAND_FIND: // Find (page 52)
// Nothing to do here
break;
case EZO_PMP_COMMAND_TYPE_READ:
case EZO_PMP_COMMAND_NONE:
default:
ESP_LOGE(TAG, "Unsupported command received: %d", this->current_command_);
return;
}
this->clear_current_command_();
}
void EzoPMP::send_next_command_() {
int wait_time_for_command = 400; // milliseconds
uint8_t command_buffer[21];
int command_buffer_length = 0;
this->pop_next_command_(); // this->next_command will be updated.
switch (this->next_command_) {
// Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54
command_buffer_length = sprintf((char *) command_buffer, "D,?");
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
command_buffer_length = sprintf((char *) command_buffer, "R");
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE:
command_buffer_length = sprintf((char *) command_buffer, "DC,?");
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "P,?");
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "TV,?");
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "ATV,?");
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "Cal,?");
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE:
command_buffer_length = sprintf((char *) command_buffer, "PV,?");
break;
// Non-Read Commands
case EZO_PMP_COMMAND_FIND: // Find (page 52)
command_buffer_length = sprintf((char *) command_buffer, "Find");
wait_time_for_command = 60000; // This command will block all updates for a minute
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
command_buffer_length = sprintf((char *) command_buffer, "D,*");
break;
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
command_buffer_length = sprintf((char *) command_buffer, "Clear");
break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,clear");
break;
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
command_buffer_length = sprintf((char *) command_buffer, "P");
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
command_buffer_length = sprintf((char *) command_buffer, "X");
break;
// Non-Read commands with parameters
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length =
sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length =
sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73)
command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_);
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_);
ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer);
break;
case EZO_PMP_COMMAND_TYPE_READ:
case EZO_PMP_COMMAND_NONE:
default:
ESP_LOGE(TAG, "Unsupported command received: %d", this->next_command_);
return;
}
// Send command
ESP_LOGV(TAG, "Sending command to device: %s", (char *) command_buffer);
this->write(command_buffer, command_buffer_length);
this->current_command_ = this->next_command_;
this->next_command_ = EZO_PMP_COMMAND_NONE;
this->is_waiting_ = true;
this->start_time_ = millis();
this->wait_time_ = wait_time_for_command;
}
void EzoPMP::pop_next_command_() {
if (this->next_command_queue_length_ <= 0) {
ESP_LOGE(TAG, "Tried to dequeue command from empty queue");
this->next_command_ = EZO_PMP_COMMAND_NONE;
this->next_command_volume_ = 0;
this->next_command_duration_ = 0;
return;
}
// Read from Head
this->next_command_ = this->next_command_queue_[this->next_command_queue_head_];
this->next_command_volume_ = this->next_command_volume_queue_[this->next_command_queue_head_];
this->next_command_duration_ = this->next_command_duration_queue_[this->next_command_queue_head_];
// Move positions
next_command_queue_head_++;
if (next_command_queue_head_ >= 10) {
next_command_queue_head_ = 0;
}
next_command_queue_length_--;
}
uint16_t EzoPMP::peek_next_command_() {
if (this->next_command_queue_length_ <= 0) {
return EZO_PMP_COMMAND_NONE;
}
return this->next_command_queue_[this->next_command_queue_head_];
}
void EzoPMP::queue_command_(uint16_t command, double volume, int duration, bool should_schedule) {
if (!should_schedule) {
return;
}
if (this->next_command_queue_length_ >= 10) {
ESP_LOGE(TAG, "Tried to queue command '%d' but queue is full", command);
return;
}
this->next_command_queue_[this->next_command_queue_last_] = command;
this->next_command_volume_queue_[this->next_command_queue_last_] = volume;
this->next_command_duration_queue_[this->next_command_queue_last_] = duration;
ESP_LOGV(TAG, "Queue command '%d' in position '%d'", command, next_command_queue_last_);
// Move positions
next_command_queue_last_++;
if (next_command_queue_last_ >= 10) {
next_command_queue_last_ = 0;
}
next_command_queue_length_++;
}
// Actions
void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); }
void EzoPMP::dose_continuously() {
this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
}
void EzoPMP::dose_volume(double volume) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
}
void EzoPMP::dose_volume_over_time(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
}
void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
}
void EzoPMP::set_calibration_volume(double volume) {
this->queue_command_(EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME, volume, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true);
}
void EzoPMP::clear_total_volume_dosed() {
this->queue_command_(EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, true);
}
void EzoPMP::clear_calibration() {
this->queue_command_(EZO_PMP_COMMAND_CLEAR_CALIBRATION, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true);
}
void EzoPMP::pause_dosing() {
this->queue_command_(EZO_PMP_COMMAND_PAUSE_DOSING, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true);
}
void EzoPMP::stop_dosing() { this->queue_command_(EZO_PMP_COMMAND_STOP_DOSING, 0, 0, true); }
void EzoPMP::change_i2c_address(int address) {
this->queue_command_(EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS, 0, address, true);
}
void EzoPMP::exec_arbitrary_command(const std::basic_string<char> &command) {
this->arbitrary_command_ = command.c_str();
this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true);
}
} // namespace ezo_pmp
} // namespace esphome

View File

@ -0,0 +1,252 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/i2c/i2c.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome {
namespace ezo_pmp {
class EzoPMP : public PollingComponent, public i2c::I2CDevice {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; };
void loop() override;
void update() override;
#ifdef USE_SENSOR
void set_current_volume_dosed(sensor::Sensor *current_volume_dosed) { current_volume_dosed_ = current_volume_dosed; }
void set_total_volume_dosed(sensor::Sensor *total_volume_dosed) { total_volume_dosed_ = total_volume_dosed; }
void set_absolute_total_volume_dosed(sensor::Sensor *absolute_total_volume_dosed) {
absolute_total_volume_dosed_ = absolute_total_volume_dosed;
}
void set_pump_voltage(sensor::Sensor *pump_voltage) { pump_voltage_ = pump_voltage; }
void set_last_volume_requested(sensor::Sensor *last_volume_requested) {
last_volume_requested_ = last_volume_requested;
}
void set_max_flow_rate(sensor::Sensor *max_flow_rate) { max_flow_rate_ = max_flow_rate; }
#endif
#ifdef USE_BINARY_SENSOR
void set_is_dosing(binary_sensor::BinarySensor *is_dosing) { is_dosing_ = is_dosing; }
void set_is_paused(binary_sensor::BinarySensor *is_paused) { is_paused_ = is_paused; }
#endif
#ifdef USE_TEXT_SENSOR
void set_dosing_mode(text_sensor::TextSensor *dosing_mode) { dosing_mode_ = dosing_mode; }
void set_calibration_status(text_sensor::TextSensor *calibration_status) { calibration_status_ = calibration_status; }
#endif
// Actions for EZO-PMP
void find();
void dose_continuously();
void dose_volume(double volume);
void dose_volume_over_time(double volume, int duration);
void dose_with_constant_flow_rate(double volume, int duration);
void set_calibration_volume(double volume);
void clear_total_volume_dosed();
void clear_calibration();
void pause_dosing();
void stop_dosing();
void change_i2c_address(int address);
void exec_arbitrary_command(const std::basic_string<char> &command);
protected:
uint32_t start_time_ = 0;
uint32_t wait_time_ = 0;
bool is_waiting_ = false;
bool is_first_read_ = true;
uint16_t next_command_ = 0;
double next_command_volume_ = 0; // might be negative
int next_command_duration_ = 0;
uint16_t next_command_queue_[10];
double next_command_volume_queue_[10];
int next_command_duration_queue_[10];
int next_command_queue_head_ = 0;
int next_command_queue_last_ = 0;
int next_command_queue_length_ = 0;
uint16_t current_command_ = 0;
bool is_paused_flag_ = false;
bool is_dosing_flag_ = false;
const char *arbitrary_command_{nullptr};
void send_next_command_();
void read_command_result_();
void clear_current_command_();
void queue_command_(uint16_t command, double volume, int duration, bool should_schedule);
void pop_next_command_();
uint16_t peek_next_command_();
#ifdef USE_SENSOR
sensor::Sensor *current_volume_dosed_{nullptr};
sensor::Sensor *total_volume_dosed_{nullptr};
sensor::Sensor *absolute_total_volume_dosed_{nullptr};
sensor::Sensor *pump_voltage_{nullptr};
sensor::Sensor *max_flow_rate_{nullptr};
sensor::Sensor *last_volume_requested_{nullptr};
#endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *is_dosing_{nullptr};
binary_sensor::BinarySensor *is_paused_{nullptr};
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *dosing_mode_{nullptr};
text_sensor::TextSensor *calibration_status_{nullptr};
#endif
};
// Action Templates
template<typename... Ts> class EzoPMPFindAction : public Action<Ts...> {
public:
EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->find(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPDoseContinuouslyAction : public Action<Ts...> {
public:
EzoPMPDoseContinuouslyAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->dose_continuously(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPDoseVolumeAction : public Action<Ts...> {
public:
EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); }
TEMPLATABLE_VALUE(double, volume)
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPDoseVolumeOverTimeAction : public Action<Ts...> {
public:
EzoPMPDoseVolumeOverTimeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override {
this->ezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...));
}
TEMPLATABLE_VALUE(double, volume)
TEMPLATABLE_VALUE(int, duration)
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPDoseWithConstantFlowRateAction : public Action<Ts...> {
public:
EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override {
this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...));
}
TEMPLATABLE_VALUE(double, volume)
TEMPLATABLE_VALUE(int, duration)
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPSetCalibrationVolumeAction : public Action<Ts...> {
public:
EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); }
TEMPLATABLE_VALUE(double, volume)
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPClearTotalVolumeDispensedAction : public Action<Ts...> {
public:
EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPClearCalibrationAction : public Action<Ts...> {
public:
EzoPMPClearCalibrationAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->clear_calibration(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPPauseDosingAction : public Action<Ts...> {
public:
EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->pause_dosing(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPStopDosingAction : public Action<Ts...> {
public:
EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->stop_dosing(); }
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPChangeI2CAddressAction : public Action<Ts...> {
public:
EzoPMPChangeI2CAddressAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); }
TEMPLATABLE_VALUE(int, address)
protected:
EzoPMP *ezopmp_;
};
template<typename... Ts> class EzoPMPArbitraryCommandAction : public Action<Ts...> {
public:
EzoPMPArbitraryCommandAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {}
void play(Ts... x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); }
TEMPLATABLE_VALUE(std::string, command)
protected:
EzoPMP *ezopmp_;
};
} // namespace ezo_pmp
} // namespace esphome

View File

@ -0,0 +1,104 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC,
ENTITY_CATEGORY_NONE,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
CONF_ID,
UNIT_VOLT,
)
from . import EzoPMP
DEPENDENCIES = ["ezo_pmp"]
CONF_CURRENT_VOLUME_DOSED = "current_volume_dosed"
CONF_TOTAL_VOLUME_DOSED = "total_volume_dosed"
CONF_ABSOLUTE_TOTAL_VOLUME_DOSED = "absolute_total_volume_dosed"
CONF_PUMP_VOLTAGE = "pump_voltage"
CONF_LAST_VOLUME_REQUESTED = "last_volume_requested"
CONF_MAX_FLOW_RATE = "max_flow_rate"
UNIT_MILILITER = "ml"
UNIT_MILILITERS_PER_MINUTE = "ml/min"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(EzoPMP),
cv.Optional(CONF_CURRENT_VOLUME_DOSED): sensor.sensor_schema(
unit_of_measurement=UNIT_MILILITER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_NONE,
),
cv.Optional(CONF_LAST_VOLUME_REQUESTED): sensor.sensor_schema(
unit_of_measurement=UNIT_MILILITER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_NONE,
),
cv.Optional(CONF_MAX_FLOW_RATE): sensor.sensor_schema(
unit_of_measurement=UNIT_MILILITERS_PER_MINUTE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_NONE,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_TOTAL_VOLUME_DOSED): sensor.sensor_schema(
unit_of_measurement=UNIT_MILILITER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_ABSOLUTE_TOTAL_VOLUME_DOSED): sensor.sensor_schema(
unit_of_measurement=UNIT_MILILITER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_PUMP_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if CONF_CURRENT_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_CURRENT_VOLUME_DOSED])
cg.add(parent.set_current_volume_dosed(sens))
if CONF_LAST_VOLUME_REQUESTED in config:
sens = await sensor.new_sensor(config[CONF_LAST_VOLUME_REQUESTED])
cg.add(parent.set_last_volume_requested(sens))
if CONF_TOTAL_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_VOLUME_DOSED])
cg.add(parent.set_total_volume_dosed(sens))
if CONF_ABSOLUTE_TOTAL_VOLUME_DOSED in config:
sens = await sensor.new_sensor(config[CONF_ABSOLUTE_TOTAL_VOLUME_DOSED])
cg.add(parent.set_absolute_total_volume_dosed(sens))
if CONF_PUMP_VOLTAGE in config:
sens = await sensor.new_sensor(config[CONF_PUMP_VOLTAGE])
cg.add(parent.set_pump_voltage(sens))
if CONF_MAX_FLOW_RATE in config:
sens = await sensor.new_sensor(config[CONF_MAX_FLOW_RATE])
cg.add(parent.set_max_flow_rate(sens))

View File

@ -0,0 +1,39 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import (
ENTITY_CATEGORY_NONE,
ENTITY_CATEGORY_DIAGNOSTIC,
CONF_ID,
)
from . import EzoPMP
DEPENDENCIES = ["ezo_pmp"]
CONF_DOSING_MODE = "dosing_mode"
CONF_CALIBRATION_STATUS = "calibration_status"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(EzoPMP),
cv.Optional(CONF_DOSING_MODE): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_NONE,
),
cv.Optional(CONF_CALIBRATION_STATUS): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if CONF_DOSING_MODE in config:
sens = await text_sensor.new_text_sensor(config[CONF_DOSING_MODE])
cg.add(parent.set_dosing_mode(sens))
if CONF_CALIBRATION_STATUS in config:
sens = await text_sensor.new_text_sensor(config[CONF_CALIBRATION_STATUS])
cg.add(parent.set_calibration_status(sens))

View File

@ -8,6 +8,7 @@ from esphome.const import (
CONF_PROTOCOL,
CONF_VISUAL,
)
from esphome.core import CORE
CODEOWNERS = ["@rob-deutsch"]
@ -115,3 +116,6 @@ def to_code(config):
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.20")
if CORE.is_esp8266 or CORE.is_esp32:
cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12")

View File

@ -14,7 +14,7 @@ static const char *const TAG = "i2c.arduino";
void ArduinoI2CBus::setup() {
recover_();
#ifdef USE_ESP32
#if defined(USE_ESP32)
static uint8_t next_bus_num = 0;
if (next_bus_num == 0) {
wire_ = &Wire;
@ -22,11 +22,25 @@ void ArduinoI2CBus::setup() {
wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory)
}
next_bus_num++;
#else
#elif defined(USE_ESP8266)
wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer)
#elif defined(USE_RP2040)
static bool first = true;
if (first) {
wire_ = &Wire;
first = false;
} else {
wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory)
}
#endif
#ifdef USE_RP2040
wire_->setSDA(this->sda_pin_);
wire_->setSCL(this->scl_pin_);
wire_->begin();
#else
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
#endif
wire_->setClock(frequency_);
initialized_ = true;
if (this->scan_) {

View File

@ -37,4 +37,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_library("esphome/Improv", "1.2.1")
cg.add_library("esphome/Improv", "1.2.3")

View File

@ -0,0 +1,74 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_DIMENSIONS,
)
from esphome.core.entity_helpers import inherit_property_from
from esphome.components import lcd_base
from esphome.components.display_menu_base import (
DISPLAY_MENU_BASE_SCHEMA,
DisplayMenuComponent,
display_menu_to_code,
)
CODEOWNERS = ["@numo68"]
AUTO_LOAD = ["display_menu_base"]
lcd_menu_ns = cg.esphome_ns.namespace("lcd_menu")
CONF_DISPLAY_ID = "display_id"
CONF_MARK_SELECTED = "mark_selected"
CONF_MARK_EDITING = "mark_editing"
CONF_MARK_SUBMENU = "mark_submenu"
CONF_MARK_BACK = "mark_back"
MINIMUM_COLUMNS = 12
LCDCharacterMenuComponent = lcd_menu_ns.class_(
"LCDCharacterMenuComponent", DisplayMenuComponent
)
MULTI_CONF = True
def validate_lcd_dimensions(config):
if config[CONF_DIMENSIONS][0] < MINIMUM_COLUMNS:
raise cv.Invalid(
f"LCD display must have at least {MINIMUM_COLUMNS} columns to be usable with the menu"
)
return config
CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LCDCharacterMenuComponent),
cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(lcd_base.LCDDisplay),
cv.Optional(CONF_MARK_SELECTED, default=0x3E): cv.uint8_t,
cv.Optional(CONF_MARK_EDITING, default=0x2A): cv.uint8_t,
cv.Optional(CONF_MARK_SUBMENU, default=0x7E): cv.uint8_t,
cv.Optional(CONF_MARK_BACK, default=0x5E): cv.uint8_t,
}
)
)
FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_DIMENSIONS, CONF_DISPLAY_ID),
validate_lcd_dimensions,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
cg.add(var.set_display(disp))
cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
await display_menu_to_code(var, config)
cg.add(var.set_mark_selected(config[CONF_MARK_SELECTED]))
cg.add(var.set_mark_editing(config[CONF_MARK_EDITING]))
cg.add(var.set_mark_submenu(config[CONF_MARK_SUBMENU]))
cg.add(var.set_mark_back(config[CONF_MARK_BACK]))

View File

@ -0,0 +1,74 @@
#include "lcd_menu.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome {
namespace lcd_menu {
static const char *const TAG = "lcd_menu";
void LCDCharacterMenuComponent::setup() {
if (this->display_->is_failed()) {
this->mark_failed();
return;
}
display_menu_base::DisplayMenuComponent::setup();
}
float LCDCharacterMenuComponent::get_setup_priority() const { return setup_priority::PROCESSOR - 1.0f; }
void LCDCharacterMenuComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LCD Menu");
ESP_LOGCONFIG(TAG, " Columns: %u, Rows: %u", this->columns_, this->rows_);
ESP_LOGCONFIG(TAG, " Mark characters: %02x, %02x, %02x, %02x", this->mark_selected_, this->mark_editing_,
this->mark_submenu_, this->mark_back_);
if (this->is_failed()) {
ESP_LOGE(TAG, "The connected display failed, the menu is disabled!");
}
}
void LCDCharacterMenuComponent::draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) {
char data[this->columns_ + 1]; // Bounded to 65 through the config
memset(data, ' ', this->columns_);
if (selected) {
data[0] = (this->editing_ || (this->mode_ == display_menu_base::MENU_MODE_JOYSTICK && item->get_immediate_edit()))
? this->mark_editing_
: this->mark_selected_;
}
switch (item->get_type()) {
case display_menu_base::MENU_ITEM_MENU:
data[this->columns_ - 1] = this->mark_submenu_;
break;
case display_menu_base::MENU_ITEM_BACK:
data[this->columns_ - 1] = this->mark_back_;
break;
default:
break;
}
auto text = item->get_text();
size_t n = std::min(text.size(), (size_t) this->columns_ - 2);
memcpy(data + 1, item->get_text().c_str(), n);
if (item->has_value()) {
std::string value = item->get_value_text();
// Maximum: start mark, at least two chars of label, space, '[', value, ']',
// end mark. Config guarantees columns >= 12
size_t val_width = std::min((size_t) this->columns_ - 7, value.length());
memcpy(data + this->columns_ - val_width - 4, " [", 2);
memcpy(data + this->columns_ - val_width - 2, value.c_str(), val_width);
data[this->columns_ - 2] = ']';
}
data[this->columns_] = '\0';
this->display_->print(0, row, data);
}
} // namespace lcd_menu
} // namespace esphome

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/components/lcd_base/lcd_display.h"
#include "esphome/components/display_menu_base/display_menu_base.h"
#include <forward_list>
#include <vector>
namespace esphome {
namespace lcd_menu {
/** Class to display a hierarchical menu.
*
*/
class LCDCharacterMenuComponent : public display_menu_base::DisplayMenuComponent {
public:
void set_display(lcd_base::LCDDisplay *display) { this->display_ = display; }
void set_dimensions(uint8_t columns, uint8_t rows) {
this->columns_ = columns;
set_rows(rows);
}
void set_mark_selected(uint8_t c) { this->mark_selected_ = c; }
void set_mark_editing(uint8_t c) { this->mark_editing_ = c; }
void set_mark_submenu(uint8_t c) { this->mark_submenu_ = c; }
void set_mark_back(uint8_t c) { this->mark_back_ = c; }
void setup() override;
float get_setup_priority() const override;
void dump_config() override;
protected:
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override;
void update() override { this->display_->update(); }
lcd_base::LCDDisplay *display_;
uint8_t columns_;
char mark_selected_;
char mark_editing_;
char mark_submenu_;
char mark_back_;
};
} // namespace lcd_menu
} // namespace esphome

View File

@ -77,6 +77,8 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG]
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
HARDWARE_UART_TO_UART_SELECTION = {
UART0: logger_ns.UART_SELECTION_UART0,
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
@ -97,15 +99,16 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
def uart_selection(value):
if value.upper() in ESP_IDF_UARTS:
if not CORE.using_esp_idf:
raise cv.Invalid(f"Only esp-idf framework supports {value}.")
if CORE.is_esp32:
if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf:
raise cv.Invalid(f"Only esp-idf framework supports {value}.")
variant = get_esp32_variant()
if variant in UART_SELECTION_ESP32:
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
if CORE.is_esp8266:
return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
if CORE.is_rp2040:
return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value)
raise NotImplementedError
@ -133,7 +136,12 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection,
cv.SplitDefault(
CONF_HARDWARE_UART,
esp8266=UART0,
esp32=UART0,
rp2040=USB_CDC,
): uart_selection,
cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
cv.Optional(CONF_LOGS, default={}): cv.Schema(
{
@ -158,12 +166,13 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(90.0)
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
rhs = Logger.new(
baud_rate,
config[CONF_TX_BUFFER_SIZE],
HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]],
)
log = cg.Pvariable(config[CONF_ID], rhs)
log = cg.new_Pvariable(config[CONF_ID], baud_rate, config[CONF_TX_BUFFER_SIZE])
if CONF_HARDWARE_UART in config:
cg.add(
log.set_uart_selection(
HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]]
)
)
cg.add(log.pre_setup())
for tag, level in config[CONF_LOGS].items():

View File

@ -1,15 +1,15 @@
#include "logger.h"
#ifdef USE_ESP_IDF
#include "freertos/FreeRTOS.h"
#include <driver/uart.h>
#endif
#include "freertos/FreeRTOS.h"
#endif // USE_ESP_IDF
#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF)
#include <esp_log.h>
#endif
#include "esphome/core/log.h"
#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace logger {
@ -148,8 +148,7 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
this->log_callback_.call(level, tag, msg);
}
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart)
: baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) {
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
// add 1 to buffer size for null terminator
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
}
@ -162,8 +161,13 @@ void Logger::pre_setup() {
#ifdef USE_ESP8266
case UART_SELECTION_UART0_SWAP:
#endif
#ifdef USE_RP2040
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
#endif
#ifdef USE_ESP8266
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
@ -172,8 +176,13 @@ void Logger::pre_setup() {
#endif
break;
case UART_SELECTION_UART1:
#ifdef USE_RP2040
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
#endif
#ifdef USE_ESP8266
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#endif
@ -184,6 +193,12 @@ void Logger::pre_setup() {
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
#ifdef USE_RP2040
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
#endif
}
#endif // USE_ARDUINO
@ -270,6 +285,9 @@ const char *const UART_SELECTIONS[] = {
#endif // USE_ESP32
#ifdef USE_ESP8266
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
#endif
#ifdef USE_RP2040
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
#endif // USE_ESP8266
void Logger::dump_config() {
ESP_LOGCONFIG(TAG, "Logger:");

View File

@ -7,11 +7,18 @@
#include <cstdarg>
#ifdef USE_ARDUINO
#if defined(USE_ESP8266) || defined(USE_ESP32)
#include <HardwareSerial.h>
#endif
#endif // USE_ESP8266 || USE_ESP32
#ifdef USE_RP2040
#include <HardwareSerial.h>
#include <SerialUSB.h>
#endif // USE_RP2040
#endif // USE_ARDUINO
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#endif
#endif // USE_ESP_IDF
namespace esphome {
@ -27,24 +34,27 @@ enum UARTSelection {
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_UART2,
#endif
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
#ifdef USE_ESP_IDF
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_USB_CDC,
#endif
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_USB_SERIAL_JTAG,
#endif
#endif
#endif
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
#endif // USE_ESP_IDF
#endif // USE_ESP32
#ifdef USE_ESP8266
UART_SELECTION_UART0_SWAP,
#endif
#endif // USE_ESP8266
#ifdef USE_RP2040
UART_SELECTION_USB_CDC,
#endif // USE_RP2040
};
class Logger : public Component {
public:
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart);
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size);
/// Manually set the baud rate for serial, set to 0 to disable.
void set_baud_rate(uint32_t baud_rate);
@ -56,6 +66,7 @@ class Logger : public Component {
uart_port_t get_uart_num() const { return uart_num_; }
#endif
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger.
UARTSelection get_uart() const;

View File

@ -6,7 +6,7 @@
namespace esphome {
namespace md5 {
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_RP2040)
void MD5Digest::init() {
memset(this->digest_, 0, 16);
MD5Init(&this->ctx_);
@ -15,7 +15,7 @@ void MD5Digest::init() {
void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); }
void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); }
#endif // USE_ARDUINO
#endif // USE_ARDUINO && !USE_RP2040
#ifdef USE_ESP_IDF
void MD5Digest::init() {
@ -28,6 +28,17 @@ void MD5Digest::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&this-
void MD5Digest::calculate() { esp_rom_md5_final(this->digest_, &this->ctx_); }
#endif // USE_ESP_IDF
#ifdef USE_RP2040
void MD5Digest::init() {
memset(this->digest_, 0, 16);
br_md5_init(&this->ctx_);
}
void MD5Digest::add(const uint8_t *data, size_t len) { br_md5_update(&this->ctx_, data, len); }
void MD5Digest::calculate() { br_md5_out(&this->ctx_, this->digest_); }
#endif // USE_RP2040
void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); }
void MD5Digest::get_hex(char *output) {

View File

@ -17,6 +17,11 @@
#define MD5_CTX_TYPE md5_context_t
#endif
#ifdef USE_RP2040
#include <MD5Builder.h>
#define MD5_CTX_TYPE br_md5_context
#endif
namespace esphome {
namespace md5 {

View File

@ -36,6 +36,8 @@ async def to_code(config):
cg.add_library("ESPmDNS", None)
elif CORE.is_esp8266:
cg.add_library("ESP8266mDNS", None)
elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None)
if config[CONF_DISABLED]:
return

View File

@ -38,6 +38,9 @@ void MDNSComponent::compile_records_() {
#endif
#ifdef USE_ESP32
platform = "ESP32";
#endif
#ifdef USE_RP2040
platform = "RP2040";
#endif
if (platform != nullptr) {
service.txt_records.push_back({"platform", platform});

View File

@ -28,7 +28,7 @@ class MDNSComponent : public Component {
void setup() override;
void dump_config() override;
#if defined(USE_ESP8266) && defined(USE_ARDUINO)
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO)
void loop() override;
#endif
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@ -0,0 +1,44 @@
#ifdef USE_RP2040
#include "esphome/components/network/ip_address.h"
#include "esphome/components/network/util.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
#include <ESP8266mDNS.h>
namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
this->compile_records_();
network::IPAddress addr = network::get_ip_address();
MDNS.begin(this->hostname_.c_str(), (uint32_t) addr);
for (const auto &service : this->services_) {
// Strip the leading underscore from the proto and service_type. While it is
// part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself.
auto *proto = service.proto.c_str();
while (*proto == '_') {
proto++;
}
auto *service_type = service.service_type.c_str();
while (*service_type == '_') {
service_type++;
}
MDNS.addService(service_type, proto, service.port);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str());
}
}
}
void MDNSComponent::loop() { MDNS.update(); }
} // namespace mdns
} // namespace esphome
#endif

View File

@ -250,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
}
),
validate_config,
cv.only_on(["esp32", "esp8266"]),
)

View File

@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(OTAComponent),
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port,
cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232, rp2040=2040): cv.port,
cv.Optional(CONF_PASSWORD): cv.string,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="5min"
@ -94,6 +94,9 @@ async def to_code(config):
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("Update", None)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("Updater", None)
use_state_callback = False
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@ -0,0 +1,59 @@
#include "esphome/core/defines.h"
#ifdef USE_ARDUINO
#ifdef USE_RP2040
#include "esphome/components/rp2040/preferences.h"
#include "ota_backend.h"
#include "ota_backend_arduino_rp2040.h"
#include "ota_component.h"
#include <Updater.h>
namespace esphome {
namespace ota {
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
bool ret = Update.begin(image_size, U_FLASH);
if (ret) {
rp2040::preferences_prevent_write(true);
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
if (error == UPDATE_ERROR_BOOTSTRAP)
return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
if (error == UPDATE_ERROR_NEW_FLASH_CONFIG)
return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
if (error == UPDATE_ERROR_FLASH_CONFIG)
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE)
return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE;
return OTA_RESPONSE_ERROR_UNKNOWN;
}
void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
return OTA_RESPONSE_OK;
}
OTAResponseTypes ArduinoRP2040OTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
}
void ArduinoRP2040OTABackend::abort() {
Update.end();
rp2040::preferences_prevent_write(false);
}
} // namespace ota
} // namespace esphome
#endif // USE_RP2040
#endif // USE_ARDUINO

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ARDUINO
#ifdef USE_RP2040
#include "esphome/core/macros.h"
#include "ota_backend.h"
#include "ota_component.h"
namespace esphome {
namespace ota {
class ArduinoRP2040OTABackend : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
};
} // namespace ota
} // namespace esphome
#endif // USE_RP2040
#endif // USE_ARDUINO

View File

@ -2,6 +2,7 @@
#include "ota_backend.h"
#include "ota_backend_arduino_esp32.h"
#include "ota_backend_arduino_esp8266.h"
#include "ota_backend_arduino_rp2040.h"
#include "ota_backend_esp_idf.h"
#include "esphome/core/log.h"
@ -35,6 +36,9 @@ std::unique_ptr<OTABackend> make_ota_backend() {
#ifdef USE_ESP_IDF
return make_unique<IDFOTABackend>();
#endif // USE_ESP_IDF
#ifdef USE_RP2040
return make_unique<ArduinoRP2040OTABackend>();
#endif // USE_RP2040
}
OTAComponent::OTAComponent() { global_ota_component = this; }

View File

@ -33,6 +33,7 @@ enum OTAResponseTypes {
OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137,
OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138,
OTA_RESPONSE_ERROR_MD5_MISMATCH = 139,
OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140,
OTA_RESPONSE_ERROR_UNKNOWN = 255,
};

View File

@ -103,7 +103,7 @@ CONFIG_SCHEMA = (
def auto_data_rate(config):
interval_sec = config[CONF_UPDATE_INTERVAL].seconds
interval_sec = config[CONF_UPDATE_INTERVAL].total_milliseconds / 1000
interval_hz = 1.0 / interval_sec
for datarate in sorted(QMC5883LDatarates.keys()):
if float(datarate) >= interval_hz:

View File

@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["binary_sensor"]
MULTI_CONF = True
rdm6300_ns = cg.esphome_ns.namespace("rdm6300")
RDM6300Component = rdm6300_ns.class_("RDM6300Component", cg.Component, uart.UARTDevice)

View File

@ -0,0 +1,161 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_FRAMEWORK,
CONF_SOURCE,
CONF_VERSION,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
)
from esphome.core import CORE, coroutine_with_priority
from .const import KEY_BOARD, KEY_RP2040, rp2040_ns
# force import gpio to register pin schema
from .gpio import rp2040_pin_to_code # noqa
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = []
def set_core_data(config):
CORE.data[KEY_RP2040] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "rp2040"
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
return config
def _format_framework_arduino_version(ver: cv.Version) -> str:
# The most recent releases have not been uploaded to platformio so grabbing them directly from
# the GitHub release is one path forward for now.
return f"https://github.com/earlephilhower/arduino-pico/releases/download/{ver}/rp2040-{ver}.zip"
# format the given arduino (https://github.com/earlephilhower/arduino-pico/releases) version to
# a PIO earlephilhower/framework-arduinopico value
# List of package versions: https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
# return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# NOTE: Keep this in mind when updating the recommended version:
# * The new version needs to be thoroughly validated before changing the
# recommended version as otherwise a bunch of devices could be bricked
# * For all constants below, update platformio.ini (in this repo)
# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 6, 2)
# The platformio/raspberrypi version to use for arduino frameworks
# - https://github.com/platformio/platform-raspberrypi/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi
ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0)
def _arduino_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(2, 6, 2), "https://github.com/earlephilhower/arduino-pico"),
"latest": (cv.Version(2, 6, 2), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
version, source = lookups[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION))
)
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning(
"The selected Arduino framework version is not the recommended one."
)
return value
def _parse_platform_version(value):
try:
# if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value)
return f"platformio/raspberrypi @ {value}"
except cv.Invalid:
return value
CONF_PLATFORM_VERSION = "platform_version"
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
}
),
_arduino_check_versions,
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
}
),
set_core_data,
)
@coroutine_with_priority(1000)
async def to_code(config):
cg.add(rp2040_ns.setup_preferences())
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO")
# cg.add_build_flag("-DPICO_BOARD=pico_w")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option(
"platform_packages",
[f"earlephilhower/framework-arduinopico @ {conf[CONF_SOURCE]}"],
)
cg.add_platformio_option("board_build.core", "earlephilhower")
cg.add_platformio_option("board_build.filesystem_size", "1m")
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
cg.add_define(
"USE_ARDUINO_VERSION_CODE",
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
)

View File

@ -0,0 +1,19 @@
RP2040_BASE_PINS = {}
RP2040_BOARD_PINS = {
"pico": {
"SDA": 4,
"SCL": 5,
"LED": 25,
"SDA1": 26,
"SCL1": 27,
},
"rpipico": "pico",
"rpipicow": {
"SDA": 4,
"SCL": 5,
"LED": 32,
"SDA1": 26,
"SCL1": 27,
},
}

View File

@ -0,0 +1,6 @@
import esphome.codegen as cg
KEY_BOARD = "board"
KEY_RP2040 = "rp2040"
rp2040_ns = cg.esphome_ns.namespace("rp2040")

View File

@ -0,0 +1,33 @@
#ifdef USE_RP2040
#include "core.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "hardware/watchdog.h"
namespace esphome {
void IRAM_ATTR HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
watchdog_reboot(0, 0, 10);
while (1) {
continue;
}
}
void arch_init() { watchdog_enable(0x7fffff, false); }
void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,14 @@
#pragma once
#ifdef USE_RP2040
#include <Arduino.h>
#include <pico.h>
extern "C" unsigned long ulMainGetRunTimeCounterValue();
namespace esphome {
namespace rp2040 {} // namespace rp2040
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,103 @@
#ifdef USE_RP2040
#include "gpio.h"
#include "esphome/core/log.h"
namespace esphome {
namespace rp2040 {
static const char *const TAG = "rp2040";
static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) {
if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone)
return INPUT;
} else if (flags == gpio::FLAG_OUTPUT) {
return OUTPUT;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
return INPUT_PULLUP;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) {
return INPUT_PULLDOWN;
// } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
// return OpenDrain;
} else {
return 0;
}
}
struct ISRPinArg {
uint8_t pin;
bool inverted;
};
ISRInternalGPIOPin RP2040GPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
return ISRInternalGPIOPin((void *) arg);
}
void RP2040GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
PinStatus arduino_mode = LOW;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
arduino_mode = inverted_ ? FALLING : RISING;
break;
case gpio::INTERRUPT_FALLING_EDGE:
arduino_mode = inverted_ ? RISING : FALLING;
break;
case gpio::INTERRUPT_ANY_EDGE:
arduino_mode = CHANGE;
break;
case gpio::INTERRUPT_LOW_LEVEL:
arduino_mode = inverted_ ? HIGH : LOW;
break;
case gpio::INTERRUPT_HIGH_LEVEL:
arduino_mode = inverted_ ? LOW : HIGH;
break;
}
attachInterrupt(pin_, func, arduino_mode, arg);
}
void RP2040GPIOPin::pin_mode(gpio::Flags flags) {
pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT
}
std::string RP2040GPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
return buffer;
}
bool RP2040GPIOPin::digital_read() {
return bool(digitalRead(pin_)) != inverted_; // NOLINT
}
void RP2040GPIOPin::digital_write(bool value) {
digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT
}
void RP2040GPIOPin::detach_interrupt() const { detachInterrupt(pin_); }
} // namespace rp2040
using namespace rp2040;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
// TODO: implement
// auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
// GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
}
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT
}
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,38 @@
#pragma once
#ifdef USE_RP2040
#include <Arduino.h>
#include "esphome/core/hal.h"
namespace esphome {
namespace rp2040 {
class RP2040GPIOPin : public InternalGPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
void setup() override { pin_mode(flags_); }
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
bool is_inverted() const override { return inverted_; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace rp2040
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,110 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.core import CORE
from esphome import pins
from . import boards
from .const import KEY_BOARD, KEY_RP2040, rp2040_ns
RP2040GPIOPin = rp2040_ns.class_("RP2040GPIOPin", cg.InternalGPIOPin)
def _lookup_pin(value):
board = CORE.data[KEY_RP2040][KEY_BOARD]
board_pins = boards.RP2040_BOARD_PINS.get(board, {})
while isinstance(board_pins, str):
board_pins = boards.RP2040_BOARD_PINS[board_pins]
if value in board_pins:
return board_pins[value]
if value in boards.RP2040_BASE_PINS:
return boards.RP2040_BASE_PINS[value]
raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")
def _translate_pin(value):
if isinstance(value, dict) or value is None:
raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas "
"(with inverted and mode)."
)
if isinstance(value, int):
return value
try:
return int(value)
except ValueError:
pass
if value.startswith("GPIO"):
return cv.int_(value[len("GPIO") :].strip())
return _lookup_pin(value)
def validate_gpio_pin(value):
value = _translate_pin(value)
board = CORE.data[KEY_RP2040][KEY_BOARD]
if board == "rpipicow" and value == 32:
return value # Special case for Pico-w LED pin
if value < 0 or value > 29:
raise cv.Invalid(f"RP2040: Invalid pin number: {value}")
return value
def validate_supports(value):
board = CORE.data[KEY_RP2040][KEY_BOARD]
if board != "rpipicow" or value[CONF_NUMBER] != 32:
return value
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_open_drain = mode[CONF_OPEN_DRAIN]
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
if not is_output or is_input or is_open_drain or is_pullup or is_pulldown:
raise cv.Invalid("Only output mode is supported for Pico-w LED pin")
return value
CONF_ANALOG = "analog"
RP2040_PIN_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(RP2040GPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_ANALOG, default=False): cv.boolean,
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
}
),
validate_supports,
)
@pins.PIN_SCHEMA_REGISTRY.register("rp2040", RP2040_PIN_SCHEMA)
async def rp2040_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@ -0,0 +1,158 @@
#ifdef USE_RP2040
#include <Arduino.h>
#include <hardware/flash.h>
#include <hardware/sync.h>
#include "preferences.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace rp2040 {
static const char *const TAG = "rp2040.preferences";
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512;
extern "C" uint8_t _EEPROM_start;
template<class It> uint8_t calculate_crc(It first, It last, uint32_t type) {
std::array<uint8_t, 4> type_array = decode_value(type);
uint8_t crc = type_array[0] ^ type_array[1] ^ type_array[2] ^ type_array[3];
while (first != last) {
crc ^= (*first++);
}
return crc;
}
class RP2040PreferenceBackend : public ESPPreferenceBackend {
public:
size_t offset = 0;
uint32_t type = 0;
bool save(const uint8_t *data, size_t len) override {
std::vector<uint8_t> buffer;
buffer.resize(len + 1);
memcpy(buffer.data(), data, len);
buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type);
for (uint32_t i = 0; i < len + 1; i++) {
uint32_t j = offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t v = buffer[i];
uint8_t *ptr = &s_flash_storage[j];
if (*ptr != v)
s_flash_dirty = true;
*ptr = v;
}
return true;
}
bool load(uint8_t *data, size_t len) override {
std::vector<uint8_t> buffer;
buffer.resize(len + 1);
for (size_t i = 0; i < len + 1; i++) {
uint32_t j = offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
buffer[i] = s_flash_storage[j];
}
uint8_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type);
if (buffer[buffer.size() - 1] != crc) {
return false;
}
memcpy(data, buffer.data(), len);
return true;
}
};
class RP2040Preferences : public ESPPreferences {
public:
uint32_t current_flash_offset = 0;
RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {}
void setup() {
s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash...");
memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
uint32_t start = this->current_flash_offset;
uint32_t end = start + length + 1;
if (end > RP2040_FLASH_STORAGE_SIZE) {
return {};
}
auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = start;
pref->type = type;
current_flash_offset = end;
return {pref};
}
bool sync() override {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving preferences to flash...");
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
flash_range_program((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage, RP2040_FLASH_STORAGE_SIZE);
::rp2040.resumeOtherCore();
}
s_flash_dirty = false;
return true;
}
bool reset() override {
ESP_LOGD(TAG, "Cleaning up preferences in flash...");
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
::rp2040.resumeOtherCore();
}
s_prevent_write = true;
return true;
}
protected:
uint8_t *eeprom_sector_;
};
void setup_preferences() {
auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->setup();
global_preferences = prefs;
}
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }
} // namespace rp2040
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,14 @@
#pragma once
#ifdef USE_RP2040
namespace esphome {
namespace rp2040 {
void setup_preferences();
void preferences_prevent_write(bool prevent);
} // namespace rp2040
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,55 @@
from esphome import pins, automation
from esphome.components import output
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_FREQUENCY,
CONF_ID,
CONF_PIN,
)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["rp2040"]
rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm")
RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(RP2040PWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
@automation.register_action(
"output.rp2040_pwm.set_frequency",
SetFrequencyAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(RP2040PWM),
cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency),
}
),
)
async def rp2040_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
cg.add(var.set_frequency(template_))
return var

View File

@ -0,0 +1,45 @@
#ifdef USE_RP2040
#include "rp2040_pwm.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#include <PinNames.h>
namespace esphome {
namespace rp2040_pwm {
static const char *const TAG = "rp2040_pwm";
void RP2040PWM::setup() {
ESP_LOGCONFIG(TAG, "Setting up RP2040 PWM Output...");
this->pin_->setup();
this->pwm_ = new mbed::PwmOut((PinName) this->pin_->get_pin());
this->turn_off();
}
void RP2040PWM::dump_config() {
ESP_LOGCONFIG(TAG, "RP2040 PWM:");
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_);
LOG_FLOAT_OUTPUT(this);
}
void HOT RP2040PWM::write_state(float state) {
this->last_output_ = state;
// Also check pin inversion
if (this->pin_->is_inverted()) {
state = 1.0f - state;
}
auto total_time_us = static_cast<uint32_t>(roundf(1e6f / this->frequency_));
this->pwm_->period_us(total_time_us);
this->pwm_->write(state);
}
} // namespace rp2040_pwm
} // namespace esphome
#endif

View File

@ -0,0 +1,57 @@
#pragma once
#ifdef USE_RP2040
#include "esphome/components/output/float_output.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "drivers/PwmOut.h"
namespace esphome {
namespace rp2040_pwm {
class RP2040PWM : public output::FloatOutput, public Component {
public:
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
/// Dynamically update frequency
void update_frequency(float frequency) override {
this->set_frequency(frequency);
this->write_state(this->last_output_);
}
/// Initialize pin
void setup() override;
void dump_config() override;
/// HARDWARE setup_priority
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
void write_state(float state) override;
InternalGPIOPin *pin_;
mbed::PwmOut *pwm_;
float frequency_{1000.0};
/// Cache last output level for dynamic frequency updating
float last_output_{0.0};
};
template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
public:
SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency);
void play(Ts... x) {
float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq);
}
RP2040PWM *parent_;
};
} // namespace rp2040_pwm
} // namespace esphome
#endif // USE_RP2040

View File

@ -2,7 +2,8 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID, CONF_MODE
from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS
from esphome.core import CORE, EsphomeError
CODEOWNERS = ["@esphome/core"]
script_ns = cg.esphome_ns.namespace("script")
@ -16,6 +17,7 @@ RestartScript = script_ns.class_("RestartScript", Script)
QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component)
ParallelScript = script_ns.class_("ParallelScript", Script)
CONF_SCRIPT = "script"
CONF_SINGLE = "single"
CONF_RESTART = "restart"
CONF_QUEUED = "queued"
@ -29,6 +31,18 @@ SCRIPT_MODES = {
CONF_PARALLEL: ParallelScript,
}
PARAMETER_TYPE_TRANSLATIONS = {
"string": "std::string",
}
def get_script(script_id):
scripts = CORE.config.get(CONF_SCRIPT, {})
for script in scripts:
if script.get(CONF_ID, None) == script_id:
return script
raise cv.Invalid(f"Script id '{script_id}' not found")
def check_max_runs(value):
if CONF_MAX_RUNS not in value:
@ -47,6 +61,44 @@ def assign_declare_id(value):
return value
def parameters_to_template(args):
template_args = []
func_args = []
script_arg_names = []
for name, type_ in args.items():
array = False
if type_.endswith("[]"):
array = True
type_ = type_[:-2]
type_ = PARAMETER_TYPE_TRANSLATIONS.get(type_, type_)
if array:
type_ = f"std::vector<{type_}>"
type_ = cg.esphome_ns.namespace(type_)
template_args.append(type_)
func_args.append((type_, name))
script_arg_names.append(name)
template = cg.TemplateArguments(*template_args)
return template, func_args
def validate_parameter_name(value):
value = cv.string(value)
if value != CONF_ID:
return value
raise cv.Invalid(f"Script's parameter name cannot be {CONF_ID}")
ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]")
def validate_parameter_type(value):
value = cv.string_strict(value)
if set(value.lower()) <= ALLOWED_PARAM_TYPE_CHARSET:
return value
raise cv.Invalid("Parameter type contains invalid characters")
CONFIG_SCHEMA = automation.validate_automation(
{
# Don't declare id as cv.declare_id yet, because the ID type
@ -56,6 +108,11 @@ CONFIG_SCHEMA = automation.validate_automation(
*SCRIPT_MODES, lower=True
),
cv.Optional(CONF_MAX_RUNS): cv.positive_int,
cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
{
validate_parameter_name: validate_parameter_type,
}
),
},
extra_validators=cv.All(check_max_runs, assign_declare_id),
)
@ -65,7 +122,8 @@ async def to_code(config):
# Register all variables first, so that scripts can use other scripts
triggers = []
for conf in config:
trigger = cg.new_Pvariable(conf[CONF_ID])
template, func_args = parameters_to_template(conf[CONF_PARAMETERS])
trigger = cg.new_Pvariable(conf[CONF_ID], template)
# Add a human-readable name to the script
cg.add(trigger.set_name(conf[CONF_ID].id))
@ -75,10 +133,10 @@ async def to_code(config):
if conf[CONF_MODE] == CONF_QUEUED:
await cg.register_component(trigger, conf)
triggers.append((trigger, conf))
triggers.append((trigger, func_args, conf))
for trigger, conf in triggers:
await automation.build_automation(trigger, [], conf)
for trigger, func_args, conf in triggers:
await automation.build_automation(trigger, func_args, conf)
@automation.register_action(
@ -87,12 +145,39 @@ async def to_code(config):
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Script),
}
cv.Optional(validate_parameter_name): cv.templatable(cv.valid),
},
),
)
async def script_execute_action_to_code(config, action_id, template_arg, args):
async def get_ordered_args(config, script_params):
config_args = config.copy()
config_args.pop(CONF_ID)
# match script_args to the formal parameter order
script_args = []
for type, name in script_params:
if name not in config_args:
raise EsphomeError(
f"Missing parameter: '{name}' in script.execute {config[CONF_ID]}"
)
arg = await cg.templatable(config_args[name], args, type)
script_args.append(arg)
return script_args
script = get_script(config[CONF_ID])
params = script.get(CONF_PARAMETERS, [])
template, script_params = parameters_to_template(params)
script_args = await get_ordered_args(config, script_params)
# We need to use the parent class 'Script' as the template argument
# to match the partial specialization of the ScriptExecuteAction template
template_arg = cg.TemplateArguments(Script.template(template), *template_arg)
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
var = cg.new_Pvariable(action_id, template_arg, paren)
cg.add(var.set_args(*script_args))
return var
@automation.register_action(
@ -101,7 +186,8 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
)
async def script_stop_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
return cg.new_Pvariable(action_id, template_arg, paren)
@ -111,7 +197,8 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
)
async def script_wait_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
await cg.register_component(var, {})
return var
@ -123,5 +210,6 @@ async def script_wait_action_to_code(config, action_id, template_arg, args):
automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
)
async def script_is_running_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
return cg.new_Pvariable(condition_id, template_arg, paren)

View File

@ -6,61 +6,8 @@ namespace script {
static const char *const TAG = "script";
void SingleScript::execute() {
if (this->is_action_running()) {
ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str());
return;
}
this->trigger();
}
void RestartScript::execute() {
if (this->is_action_running()) {
ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str());
this->stop_action();
}
this->trigger();
}
void QueueingScript::execute() {
if (this->is_action_running()) {
// num_runs_ is the number of *queued* instances, so total number of instances is
// num_runs_ + 1
if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) {
ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str());
return;
}
ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str());
this->num_runs_++;
return;
}
this->trigger();
// Check if the trigger was immediate and we can continue right away.
this->loop();
}
void QueueingScript::stop() {
this->num_runs_ = 0;
Script::stop();
}
void QueueingScript::loop() {
if (this->num_runs_ != 0 && !this->is_action_running()) {
this->num_runs_--;
this->trigger();
}
}
void ParallelScript::execute() {
if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str());
return;
}
this->trigger();
void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) {
esp_log_printf_(level, TAG, line, format, param);
}
} // namespace script

View File

@ -2,27 +2,48 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome {
namespace script {
class ScriptLogger {
protected:
void esp_logw_(int line, const char *format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
}
void esp_logd_(int line, const char *format, const char *param) {
esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
}
void esp_log_(int level, int line, const char *format, const char *param);
};
/// The abstract base class for all script types.
class Script : public Trigger<> {
template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts...> {
public:
/** Execute a new instance of this script.
*
* The behavior of this function when a script is already running is defined by the subtypes
*/
virtual void execute() = 0;
virtual void execute(Ts...) = 0;
/// Check if any instance of this script is currently running.
virtual bool is_running() { return this->is_action_running(); }
/// Stop all instances of this script.
virtual void stop() { this->stop_action(); }
// execute this script using a tuple that contains the arguments
void execute_tuple(const std::tuple<Ts...> &tuple) {
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
}
// Internal function to give scripts readable names.
void set_name(const std::string &name) { name_ = name; }
protected:
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->execute(std::get<S>(tuple)...);
}
std::string name_;
};
@ -31,9 +52,16 @@ class Script : public Trigger<> {
* If a new instance is executed while the previous one hasn't finished yet,
* a warning is printed and the new instance is discarded.
*/
class SingleScript : public Script {
template<typename... Ts> class SingleScript : public Script<Ts...> {
public:
void execute() override;
void execute(Ts... x) override {
if (this->is_action_running()) {
this->esp_logw_(__LINE__, "Script '%s' is already running! (mode: single)", this->name_.c_str());
return;
}
this->trigger(x...);
}
};
/** A script type that restarts scripts from the beginning when a new instance is started.
@ -41,20 +69,55 @@ class SingleScript : public Script {
* If a new instance is started but another one is already running, the existing
* script is stopped and the new instance starts from the beginning.
*/
class RestartScript : public Script {
template<typename... Ts> class RestartScript : public Script<Ts...> {
public:
void execute() override;
void execute(Ts... x) override {
if (this->is_action_running()) {
this->esp_logd_(__LINE__, "Script '%s' restarting (mode: restart)", this->name_.c_str());
this->stop_action();
}
this->trigger(x...);
}
};
/** A script type that queues new instances that are created.
*
* Only one instance of the script can be active at a time.
*/
class QueueingScript : public Script, public Component {
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
public:
void execute() override;
void stop() override;
void loop() override;
void execute(Ts... x) override {
if (this->is_action_running()) {
// num_runs_ is the number of *queued* instances, so total number of instances is
// num_runs_ + 1
if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) {
this->esp_logw_(__LINE__, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str());
return;
}
this->esp_logd_(__LINE__, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str());
this->num_runs_++;
return;
}
this->trigger(x...);
// Check if the trigger was immediate and we can continue right away.
this->loop();
}
void stop() override {
this->num_runs_ = 0;
Script<Ts...>::stop();
}
void loop() override {
if (this->num_runs_ != 0 && !this->is_action_running()) {
this->num_runs_--;
this->trigger();
}
}
void set_max_runs(int max_runs) { max_runs_ = max_runs; }
protected:
@ -67,48 +130,84 @@ class QueueingScript : public Script, public Component {
* If a new instance is started while previous ones haven't finished yet,
* the new one is executed in parallel to the other instances.
*/
class ParallelScript : public Script {
template<typename... Ts> class ParallelScript : public Script<Ts...> {
public:
void execute() override;
void execute(Ts... x) override {
if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
this->esp_logw_(__LINE__, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str());
return;
}
this->trigger(x...);
}
void set_max_runs(int max_runs) { max_runs_ = max_runs; }
protected:
int max_runs_ = 0;
};
template<typename... Ts> class ScriptExecuteAction : public Action<Ts...> {
public:
ScriptExecuteAction(Script *script) : script_(script) {}
template<class S, typename... Ts> class ScriptExecuteAction;
void play(Ts... x) override { this->script_->execute(); }
template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, Ts...> : public Action<Ts...> {
public:
ScriptExecuteAction(Script<As...> *script) : script_(script) {}
using Args = std::tuple<TemplatableValue<As, Ts...>...>;
template<typename... F> void set_args(F... x) { args_ = Args{x...}; }
void play(Ts... x) override { this->script_->execute_tuple(this->eval_args_(x...)); }
protected:
Script *script_;
// NOTE:
// `eval_args_impl` functions evaluates `I`th the functions in `args` member.
// and then recursively calls `eval_args_impl` for the `I+1`th arg.
// if `I` = `N` all args have been stored, and nothing is done.
template<std::size_t N>
void eval_args_impl_(std::tuple<As...> & /*unused*/, std::integral_constant<std::size_t, N> /*unused*/,
std::integral_constant<std::size_t, N> /*unused*/, Ts... /*unused*/) {}
template<std::size_t I, std::size_t N>
void eval_args_impl_(std::tuple<As...> &evaled_args, std::integral_constant<std::size_t, I> /*unused*/,
std::integral_constant<std::size_t, N> n, Ts... x) {
std::get<I>(evaled_args) = std::get<I>(args_).value(x...); // NOTE: evaluate `i`th arg, and store in tuple.
eval_args_impl_(evaled_args, std::integral_constant<std::size_t, I + 1>{}, n,
x...); // NOTE: recurse to next index.
}
std::tuple<As...> eval_args_(Ts... x) {
std::tuple<As...> evaled_args;
eval_args_impl_(evaled_args, std::integral_constant<std::size_t, 0>{}, std::tuple_size<Args>{}, x...);
return evaled_args;
}
Script<As...> *script_;
Args args_;
};
template<typename... Ts> class ScriptStopAction : public Action<Ts...> {
template<class C, typename... Ts> class ScriptStopAction : public Action<Ts...> {
public:
ScriptStopAction(Script *script) : script_(script) {}
ScriptStopAction(C *script) : script_(script) {}
void play(Ts... x) override { this->script_->stop(); }
protected:
Script *script_;
C *script_;
};
template<typename... Ts> class IsRunningCondition : public Condition<Ts...> {
template<class C, typename... Ts> class IsRunningCondition : public Condition<Ts...> {
public:
explicit IsRunningCondition(Script *parent) : parent_(parent) {}
explicit IsRunningCondition(C *parent) : parent_(parent) {}
bool check(Ts... x) override { return this->parent_->is_running(); }
protected:
Script *parent_;
C *parent_;
};
template<typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component {
template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component {
public:
ScriptWaitAction(Script *script) : script_(script) {}
ScriptWaitAction(C *script) : script_(script) {}
void play_complex(Ts... x) override {
this->num_running_++;
@ -137,7 +236,7 @@ template<typename... Ts> class ScriptWaitAction : public Action<Ts...>, public C
}
protected:
Script *script_;
C *script_;
std::tuple<Ts...> var_{};
};

View File

@ -42,7 +42,7 @@ void SM300D2Sensor::update() {
this->status_clear_warning();
ESP_LOGW(TAG, "Successfully read SM300D2 data");
ESP_LOGD(TAG, "Successfully read SM300D2 data");
const uint16_t co2 = (response[2] * 256) + response[3];
const uint16_t formaldehyde = (response[4] * 256) + response[5];

View File

@ -10,6 +10,9 @@
#ifdef USE_ESP8266
#include "sntp.h"
#endif
#ifdef USE_RP2040
#include "lwip/apps/sntp.h"
#endif
// Yes, the server names are leaked, but that's fine.
#ifdef CLANG_TIDY

View File

@ -13,6 +13,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_IMPLEMENTATION,
esp8266=IMPLEMENTATION_LWIP_TCP,
esp32=IMPLEMENTATION_BSD_SOCKETS,
rp2040=IMPLEMENTATION_LWIP_TCP,
): cv.one_of(
IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_"
),

View File

@ -91,7 +91,7 @@ struct iovec {
size_t iov_len;
};
#ifdef USE_ESP8266
#if defined(USE_ESP8266) || defined(USE_RP2040)
// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define
#ifdef INADDR_ANY
#undef INADDR_ANY

View File

@ -46,9 +46,7 @@ async def to_code(config):
mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN])
cg.add(var.set_mosi(mosi))
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("SPI", None)
if CORE.is_esp8266:
if CORE.using_arduino:
cg.add_library("SPI", None)

View File

@ -105,7 +105,11 @@ class SPIComponent : public Component {
void write_byte(uint8_t data) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
#ifdef USE_RP2040
this->hw_spi_->transfer(data);
#else
this->hw_spi_->write(data);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
@ -116,7 +120,11 @@ class SPIComponent : public Component {
void write_byte16(const uint16_t data) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
#ifdef USE_RP2040
this->hw_spi_->transfer16(data);
#else
this->hw_spi_->write16(data);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
@ -130,7 +138,11 @@ class SPIComponent : public Component {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
for (size_t i = 0; i < length; i++) {
#ifdef USE_RP2040
this->hw_spi_->transfer16(data[i]);
#else
this->hw_spi_->write16(data[i]);
#endif
}
return;
}
@ -145,7 +157,11 @@ class SPIComponent : public Component {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
auto *data_c = const_cast<uint8_t *>(data);
#ifdef USE_RP2040
this->hw_spi_->transfer(data_c, length);
#else
this->hw_spi_->writeBytes(data_c, length);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
@ -178,7 +194,11 @@ class SPIComponent : public Component {
if (this->miso_ != nullptr) {
this->hw_spi_->transfer(data, length);
} else {
#ifdef USE_RP2040
this->hw_spi_->transfer(data, length);
#else
this->hw_spi_->writeBytes(data, length);
#endif
}
return;
}
@ -205,7 +225,11 @@ class SPIComponent : public Component {
} else if (CLOCK_POLARITY && CLOCK_PHASE) {
data_mode = SPI_MODE3;
}
#ifdef USE_RP2040
SPISettings settings(DATA_RATE, static_cast<BitOrder>(BIT_ORDER), data_mode);
#else
SPISettings settings(DATA_RATE, BIT_ORDER, data_mode);
#endif
this->hw_spi_->beginTransaction(settings);
} else {
#endif // USE_SPI_ARDUINO_BACKEND

View File

@ -51,7 +51,7 @@ optional<std::string> ToUpperFilter::new_value(std::string value) {
// ToLowerFilter
optional<std::string> ToLowerFilter::new_value(std::string value) {
for (char &c : value)
c = ::toupper(c);
c = ::tolower(c);
return value;
}

View File

@ -4,6 +4,9 @@
#ifdef USE_ESP8266
#include "sys/time.h"
#endif
#ifdef USE_RP2040
#include <sys/time.h>
#endif
#include <cerrno>
namespace esphome {

View File

@ -41,6 +41,7 @@ ESP32ArduinoUARTComponent = uart_ns.class_(
ESP8266UartComponent = uart_ns.class_(
"ESP8266UartComponent", UARTComponent, cg.Component
)
RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component)
UARTDevice = uart_ns.class_("UARTDevice")
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
@ -89,6 +90,8 @@ def _uart_declare_type(value):
return cv.declare_id(ESP32ArduinoUARTComponent)(value)
if CORE.using_esp_idf:
return cv.declare_id(IDFUARTComponent)(value)
if CORE.is_rp2040:
return cv.declare_id(RP2040UartComponent)(value)
raise NotImplementedError

View File

@ -90,6 +90,7 @@ void ESP32ArduinoUARTComponent::setup() {
this->hw_serial_ = &Serial;
} else {
static uint8_t next_uart_num = 1;
this->number_ = next_uart_num;
this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory)
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
@ -104,7 +105,7 @@ void ESP32ArduinoUARTComponent::setup() {
}
void ESP32ArduinoUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus:");
ESP_LOGCONFIG(TAG, "UART Bus %d:", this->number_);
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {

View File

@ -32,6 +32,7 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component {
void check_logger_conflict() override;
HardwareSerial *hw_serial_{nullptr};
uint8_t number_{0};
};
} // namespace uart

View File

@ -0,0 +1,184 @@
#ifdef USE_RP2040
#include "uart_component_rp2040.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <hardware/uart.h>
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome {
namespace uart {
static const char *const TAG = "uart.arduino_rp2040";
uint16_t RP2040UartComponent::get_config() {
uint16_t config = 0;
if (this->parity_ == UART_CONFIG_PARITY_NONE) {
config |= UART_PARITY_NONE;
} else if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
config |= UART_PARITY_EVEN;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
config |= UART_PARITY_ODD;
}
switch (this->data_bits_) {
case 5:
config |= SERIAL_DATA_5;
break;
case 6:
config |= SERIAL_DATA_6;
break;
case 7:
config |= SERIAL_DATA_7;
break;
case 8:
config |= SERIAL_DATA_8;
break;
}
if (this->stop_bits_ == 1) {
config |= SERIAL_STOP_BIT_1;
} else {
config |= SERIAL_STOP_BIT_2;
}
return config;
}
void RP2040UartComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up UART bus...");
uint16_t config = get_config();
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});
constexpr uint32_t valid_tx_uart_1 = __bitset({4, 8, 20, 24});
constexpr uint32_t valid_rx_uart_0 = __bitset({1, 13, 17, 29});
constexpr uint32_t valid_rx_uart_1 = __bitset({5, 9, 21, 25});
int8_t tx_hw = -1;
int8_t rx_hw = -1;
if (this->tx_pin_ != nullptr) {
if (this->tx_pin_->is_inverted()) {
ESP_LOGD(TAG, "An inverted TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
} else {
if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_0) != 0) {
tx_hw = 0;
} else if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_1) != 0) {
tx_hw = 1;
} else {
ESP_LOGD(TAG, "TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
}
}
}
if (this->rx_pin_ != nullptr) {
if (this->rx_pin_->is_inverted()) {
ESP_LOGD(TAG, "An inverted RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
} else {
if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_0) != 0) {
rx_hw = 0;
} else if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_1) != 0) {
rx_hw = 1;
} else {
ESP_LOGD(TAG, "RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
}
}
}
#ifdef USE_LOGGER
if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) {
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw);
tx_hw = -1;
rx_hw = -1;
}
#endif
if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) {
ESP_LOGV(TAG, "Using SerialPIO");
pin_size_t tx = this->tx_pin_ == nullptr ? SerialPIO::NOPIN : this->tx_pin_->get_pin();
pin_size_t rx = this->rx_pin_ == nullptr ? SerialPIO::NOPIN : this->rx_pin_->get_pin();
auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory)
serial->begin(this->baud_rate_, config);
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
gpio_set_outover(tx, GPIO_OVERRIDE_INVERT);
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
gpio_set_inover(rx, GPIO_OVERRIDE_INVERT);
this->serial_ = serial;
} else {
ESP_LOGV(TAG, "Using Hardware Serial");
SerialUART *serial;
if (tx_hw == 0) {
serial = &Serial1;
} else {
serial = &Serial2;
}
serial->setTX(this->tx_pin_->get_pin());
serial->setRX(this->rx_pin_->get_pin());
serial->setFIFOSize(this->rx_buffer_size_);
serial->begin(this->baud_rate_, config);
this->serial_ = serial;
this->hw_serial_ = true;
}
}
void RP2040UartComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus:");
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
}
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
if (this->hw_serial_) {
ESP_LOGCONFIG(TAG, " Using hardware serial");
} else {
ESP_LOGCONFIG(TAG, " Using SerialPIO");
}
}
void RP2040UartComponent::write_array(const uint8_t *data, size_t len) {
this->serial_->write(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool RP2040UartComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->serial_->peek();
return true;
}
bool RP2040UartComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
this->serial_->readBytes(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
int RP2040UartComponent::available() { return this->serial_->available(); }
void RP2040UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing...");
this->serial_->flush();
}
} // namespace uart
} // namespace esphome
#endif // USE_RP2040

View File

@ -0,0 +1,43 @@
#pragma once
#ifdef USE_RP2040
#include <SerialPIO.h>
#include <SerialUART.h>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome {
namespace uart {
class RP2040UartComponent : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
int available() override;
void flush() override;
uint16_t get_config();
protected:
void check_logger_conflict() override {}
bool hw_serial_{false};
HardwareSerial *serial_{nullptr};
};
} // namespace uart
} // namespace esphome
#endif // USE_RP2040

View File

@ -6,6 +6,7 @@ from esphome.const import (
STATE_CLASS_TOTAL_INCREASING,
UNIT_SECOND,
ICON_TIMER,
DEVICE_CLASS_DURATION,
)
uptime_ns = cg.esphome_ns.namespace("uptime")
@ -17,6 +18,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
icon=ICON_TIMER,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("60s"))

View File

@ -75,6 +75,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
cv.only_on(["esp32", "esp8266"]),
default_url,
validate_local,
)

View File

@ -7,581 +7,582 @@ namespace web_server {
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3,
0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x25, 0x10, 0x22, 0x29, 0xcb, 0x76, 0x81, 0x02, 0x79, 0xe5,
0xa5, 0xae, 0x5d, 0xe5, 0xad, 0x2c, 0xd9, 0x75, 0xab, 0x54, 0x2c, 0x0b, 0x22, 0x93, 0x22, 0xca, 0x20, 0xc0, 0x02,
0x92, 0x5a, 0x8a, 0x42, 0x9f, 0x7e, 0xea, 0xa7, 0x39, 0x67, 0xd6, 0x87, 0x7e, 0x99, 0xd3, 0xfd, 0x30, 0x1f, 0x31,
0xcf, 0xfd, 0x29, 0xf7, 0x07, 0xa6, 0x3f, 0x61, 0x22, 0x22, 0x17, 0x24, 0x40, 0x6a, 0x71, 0x75, 0xcd, 0x3d, 0x5e,
0x04, 0xe4, 0x1a, 0x11, 0x19, 0x19, 0x5b, 0x46, 0x42, 0xbb, 0x77, 0xc6, 0xd9, 0x88, 0x5f, 0xcc, 0x99, 0x35, 0xe5,
0xb3, 0xa4, 0xbf, 0x2b, 0xff, 0x67, 0xd1, 0xb8, 0xbf, 0x9b, 0xc4, 0xe9, 0x27, 0x2b, 0x67, 0x49, 0x18, 0x8f, 0xb2,
0xd4, 0x9a, 0xe6, 0x6c, 0x12, 0x8e, 0x23, 0x1e, 0x05, 0xf1, 0x2c, 0x3a, 0x61, 0xd6, 0x56, 0x7f, 0x77, 0xc6, 0x78,
0x64, 0x8d, 0xa6, 0x51, 0x5e, 0x30, 0x1e, 0xbe, 0x3f, 0xf8, 0xba, 0xf5, 0xa8, 0xbf, 0x5b, 0x8c, 0xf2, 0x78, 0xce,
0x2d, 0x1c, 0x32, 0x9c, 0x65, 0xe3, 0x45, 0xc2, 0xfa, 0xa7, 0x51, 0x6e, 0x9d, 0xb3, 0xf0, 0xcd, 0xf1, 0x2f, 0x6c,
0xc4, 0xfd, 0x31, 0x9b, 0xc4, 0x29, 0x7b, 0x9b, 0x67, 0x73, 0x96, 0xf3, 0x0b, 0xef, 0xd9, 0xfa, 0x8a, 0x98, 0x15,
0xde, 0xbe, 0xae, 0x3a, 0x61, 0xfc, 0xcd, 0x59, 0xaa, 0xfa, 0x3c, 0x65, 0x62, 0x92, 0x2c, 0x2f, 0x3c, 0x7e, 0x45,
0x9b, 0xfd, 0x8b, 0xd9, 0x71, 0x96, 0x14, 0xde, 0x27, 0x5d, 0x3f, 0xcf, 0x33, 0x9e, 0x21, 0x58, 0xfe, 0x34, 0x2a,
0x8c, 0x96, 0xde, 0x93, 0x35, 0x4d, 0xe6, 0xb2, 0xf2, 0x45, 0xf1, 0x2c, 0x5d, 0xcc, 0x58, 0x1e, 0x1d, 0x27, 0xcc,
0x2b, 0x58, 0xe8, 0x30, 0x8f, 0x7b, 0xb1, 0x1b, 0xf6, 0xb9, 0x15, 0xa7, 0x16, 0x1b, 0x9c, 0x33, 0x2a, 0x59, 0x32,
0xdd, 0x2a, 0xb8, 0xd3, 0xf6, 0x80, 0x5c, 0x93, 0xf8, 0x64, 0xa1, 0xdf, 0xcf, 0xf2, 0x98, 0xab, 0xe7, 0xd3, 0x28,
0x59, 0xb0, 0x20, 0x2e, 0xdd, 0x80, 0x1d, 0xf2, 0x61, 0x18, 0x7b, 0x4f, 0x68, 0x50, 0x18, 0x72, 0x39, 0xc9, 0x72,
0x07, 0x69, 0x15, 0xe3, 0xd8, 0xfc, 0xf2, 0xd2, 0xe1, 0xe1, 0xb2, 0x74, 0xdd, 0x4f, 0xcc, 0x1f, 0x45, 0x49, 0xe2,
0xe0, 0xc4, 0x77, 0xef, 0x16, 0x38, 0x63, 0xec, 0xf1, 0xc3, 0x78, 0xe8, 0xf6, 0xe2, 0x89, 0xc3, 0x99, 0x5b, 0xf5,
0xcb, 0x26, 0x16, 0x67, 0x0e, 0x77, 0xdd, 0x27, 0x57, 0xf7, 0xc9, 0x19, 0x5f, 0xe4, 0x00, 0x7b, 0xe9, 0xbd, 0x51,
0x33, 0x3f, 0xc3, 0xfa, 0x7d, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x08, 0xcf, 0xe2, 0x74, 0x9c, 0x9d, 0xf9,
0xfb, 0xd3, 0x08, 0x7e, 0xbc, 0xcb, 0x32, 0x7e, 0xf7, 0xae, 0x73, 0x9a, 0xc5, 0x63, 0xab, 0x1d, 0x86, 0x66, 0xe5,
0xc5, 0x93, 0xfd, 0xfd, 0xcb, 0xcb, 0x46, 0x81, 0x9f, 0x46, 0x3c, 0x3e, 0x65, 0xa2, 0x33, 0x00, 0x60, 0xc3, 0xcf,
0x39, 0x67, 0xe3, 0x7d, 0x7e, 0x91, 0x40, 0x29, 0x63, 0xbc, 0xb0, 0x01, 0xc7, 0xa7, 0xd9, 0x08, 0xc8, 0x96, 0x1a,
0x84, 0x87, 0xa6, 0x39, 0x9b, 0x27, 0xd1, 0x88, 0x61, 0x3d, 0x8c, 0x54, 0xf5, 0xa8, 0x1a, 0x79, 0x5f, 0x87, 0x62,
0x79, 0x1d, 0xd7, 0x8b, 0x59, 0x98, 0xb2, 0x33, 0xeb, 0x55, 0x34, 0xef, 0x8d, 0x92, 0xa8, 0x28, 0x80, 0x5f, 0x97,
0x84, 0x42, 0xbe, 0x18, 0x01, 0x83, 0x10, 0x82, 0x4b, 0x24, 0xd3, 0x34, 0x2e, 0xfc, 0x8f, 0x1b, 0xa3, 0xa2, 0x78,
0xc7, 0x8a, 0x45, 0xc2, 0x37, 0x42, 0x58, 0x0b, 0x7e, 0x27, 0x0c, 0xbf, 0x76, 0xf9, 0x34, 0xcf, 0xce, 0xac, 0x67,
0x79, 0x0e, 0xcd, 0x6d, 0x98, 0x52, 0x34, 0xb0, 0xe2, 0xc2, 0x4a, 0x33, 0x6e, 0xe9, 0xc1, 0x70, 0x01, 0x7d, 0xeb,
0x7d, 0xc1, 0xac, 0xa3, 0x45, 0x5a, 0x44, 0x13, 0x06, 0x4d, 0x8f, 0xac, 0x2c, 0xb7, 0x8e, 0x60, 0xd0, 0x23, 0x58,
0xb2, 0x82, 0xc3, 0xae, 0xf1, 0x6d, 0xb7, 0x47, 0x73, 0x41, 0xe1, 0x01, 0x3b, 0xe7, 0x21, 0x2b, 0x81, 0x31, 0xad,
0x42, 0xa3, 0xe1, 0xb8, 0xcb, 0x04, 0x0a, 0x58, 0x18, 0x33, 0x64, 0x59, 0xc7, 0x6c, 0xac, 0x17, 0xe7, 0xc3, 0xdd,
0xbb, 0x9a, 0xd6, 0x40, 0x13, 0x07, 0xda, 0x16, 0x8d, 0xb6, 0x9e, 0x40, 0xbc, 0x46, 0x22, 0xd7, 0x63, 0xbe, 0x24,
0xdf, 0xfe, 0x45, 0x3a, 0xaa, 0x8f, 0x0d, 0x95, 0x25, 0xcf, 0xf6, 0x79, 0x1e, 0xa7, 0x27, 0x00, 0x84, 0x9c, 0xc9,
0x6c, 0x52, 0x96, 0x62, 0xf1, 0xdf, 0xb0, 0x90, 0x85, 0x7d, 0x1c, 0x3d, 0x67, 0x8e, 0x5d, 0x50, 0x0f, 0x3b, 0x0c,
0x91, 0xf4, 0xc0, 0x60, 0x6c, 0xc0, 0x02, 0xb6, 0x69, 0xdb, 0xde, 0xd7, 0xae, 0x77, 0x81, 0x1c, 0xe4, 0xfb, 0x3e,
0xb1, 0xaf, 0xe8, 0x1c, 0x87, 0x1d, 0x04, 0xda, 0x4f, 0x58, 0x7a, 0xc2, 0xa7, 0x03, 0x76, 0xd8, 0x1e, 0x06, 0x1c,
0xa0, 0x1a, 0x2f, 0x46, 0xcc, 0x41, 0x7e, 0xf4, 0x72, 0xdc, 0x3e, 0x9b, 0x0e, 0x4c, 0x81, 0x0b, 0x73, 0x87, 0x70,
0xac, 0x2d, 0x8d, 0xab, 0x58, 0x54, 0x01, 0x86, 0x7c, 0x6e, 0xc3, 0x0e, 0x3b, 0x66, 0xb9, 0x01, 0x87, 0x6e, 0xd6,
0xab, 0xad, 0xe0, 0x02, 0x56, 0x08, 0xfa, 0x59, 0x93, 0x45, 0x3a, 0xe2, 0x31, 0x08, 0x2e, 0x7b, 0x13, 0xc0, 0x15,
0x2b, 0xa7, 0x17, 0xce, 0x76, 0x4b, 0xd7, 0x89, 0xdd, 0x4d, 0x76, 0x98, 0x6f, 0x76, 0x86, 0x1e, 0x42, 0xa9, 0x89,
0x2f, 0x11, 0x8f, 0x01, 0xc1, 0xd2, 0x7b, 0xcb, 0xf4, 0xf6, 0xfc, 0x30, 0x60, 0xfe, 0x2a, 0x1f, 0x87, 0xdc, 0x9f,
0x45, 0x73, 0xc4, 0x86, 0x11, 0x0f, 0x44, 0xe9, 0x08, 0xa1, 0xab, 0xad, 0x0b, 0x52, 0xcc, 0xaf, 0x58, 0xc0, 0x05,
0x82, 0xc0, 0x9e, 0x7d, 0x16, 0x8d, 0xa6, 0xb0, 0xc5, 0x2b, 0xc2, 0x8d, 0xd5, 0x76, 0x18, 0xe5, 0x2c, 0xe2, 0xec,
0x59, 0xc2, 0xf0, 0x0d, 0x57, 0x00, 0x7a, 0xda, 0xae, 0x97, 0xab, 0x7d, 0x97, 0xc4, 0xfc, 0x75, 0x06, 0xf3, 0xf4,
0x04, 0x93, 0x00, 0x17, 0xe7, 0x77, 0xef, 0xc6, 0xc8, 0x22, 0x7b, 0x1c, 0x56, 0xeb, 0x78, 0x01, 0x42, 0xc0, 0x4e,
0xb1, 0x85, 0x0d, 0xd4, 0xf6, 0x62, 0x9f, 0x03, 0x11, 0x9f, 0x64, 0x29, 0x87, 0xe1, 0x00, 0x5e, 0xcd, 0x41, 0x7e,
0x34, 0x9f, 0xb3, 0x74, 0xfc, 0x64, 0x1a, 0x27, 0x63, 0xa0, 0x46, 0x09, 0xf8, 0x66, 0x2c, 0x04, 0x3c, 0x01, 0x99,
0xe0, 0x7a, 0x8c, 0x68, 0xf9, 0x90, 0x91, 0x79, 0x68, 0xdb, 0x3d, 0x94, 0x40, 0x12, 0x0b, 0x94, 0x41, 0xb4, 0x70,
0xef, 0x40, 0xf4, 0x17, 0x2e, 0xdf, 0x0c, 0x63, 0xbd, 0x8c, 0x92, 0xc0, 0x6f, 0x50, 0xd2, 0x00, 0xfd, 0x19, 0xc8,
0xc0, 0x1e, 0x0a, 0xae, 0xef, 0xa5, 0xd4, 0x49, 0x99, 0xc2, 0x10, 0x08, 0x30, 0x42, 0x09, 0x22, 0x69, 0xf0, 0x36,
0x4b, 0x2e, 0x26, 0x71, 0x92, 0xec, 0x2f, 0xe6, 0xf3, 0x2c, 0xe7, 0xde, 0x37, 0xe1, 0x92, 0x67, 0x15, 0xae, 0xb4,
0xc9, 0x8b, 0xb3, 0x98, 0x23, 0x41, 0xdd, 0xe5, 0x28, 0x82, 0xa5, 0x7e, 0x9c, 0x65, 0x09, 0x8b, 0x52, 0x40, 0x83,
0x0d, 0x6c, 0x3b, 0x48, 0x17, 0x49, 0xd2, 0x3b, 0x86, 0x61, 0x3f, 0xf5, 0xa8, 0x5a, 0x48, 0xfc, 0x80, 0x9e, 0xf7,
0xf2, 0x3c, 0xba, 0x80, 0x86, 0xd8, 0x06, 0x78, 0x11, 0x56, 0xeb, 0x9b, 0xfd, 0x37, 0xaf, 0x7d, 0xc1, 0xf8, 0xf1,
0xe4, 0x02, 0x00, 0x2d, 0x2b, 0xa9, 0x39, 0xc9, 0xb3, 0x59, 0x63, 0x6a, 0xa4, 0x43, 0x1c, 0xb2, 0xde, 0x15, 0x20,
0xc4, 0x34, 0x32, 0xac, 0x12, 0x33, 0x21, 0x78, 0x4d, 0xfc, 0x2c, 0x2b, 0x71, 0x0f, 0x0c, 0xf0, 0x21, 0x10, 0xc5,
0x30, 0xe5, 0xf5, 0xd0, 0xf2, 0xfc, 0x62, 0x19, 0x87, 0x04, 0xe7, 0x1c, 0xf5, 0x2f, 0xc2, 0x38, 0x8a, 0x60, 0xf6,
0xa5, 0x18, 0xb0, 0x54, 0x10, 0xc7, 0x65, 0xe9, 0x25, 0x9a, 0x89, 0x51, 0xe2, 0xa1, 0x40, 0xe1, 0xb0, 0x8d, 0x2e,
0x2f, 0x19, 0xbc, 0xb8, 0xde, 0xb7, 0xe1, 0x32, 0x52, 0xf8, 0xa0, 0x86, 0xc2, 0xfd, 0x15, 0x08, 0x39, 0x81, 0x9a,
0xec, 0x14, 0xf4, 0x20, 0xc0, 0xf9, 0x8d, 0x07, 0xfa, 0x3f, 0x41, 0x28, 0xee, 0x74, 0x3c, 0xd0, 0xa0, 0x4f, 0xa6,
0x51, 0x7a, 0xc2, 0xc6, 0x41, 0xc2, 0x4a, 0x29, 0x79, 0xf7, 0x2c, 0x58, 0x63, 0x60, 0xa7, 0xc2, 0x7a, 0x7e, 0xf0,
0xea, 0xa5, 0x5c, 0xb9, 0x9a, 0x30, 0x86, 0x45, 0x5a, 0x80, 0x5a, 0x05, 0xb1, 0x2d, 0xc5, 0xf1, 0x33, 0xae, 0xa4,
0xb7, 0x28, 0x89, 0x8b, 0xf7, 0x73, 0x30, 0x31, 0xd8, 0x5b, 0x18, 0x06, 0xa6, 0x0f, 0x61, 0x2a, 0x2a, 0x87, 0xf9,
0x44, 0xc5, 0x58, 0x17, 0x41, 0x67, 0x81, 0xa9, 0x78, 0xcd, 0x1c, 0xb7, 0x04, 0x56, 0xe5, 0xf1, 0xc8, 0x8a, 0xc6,
0xe3, 0x17, 0x69, 0xcc, 0xe3, 0x28, 0x89, 0x7f, 0x23, 0x4a, 0x2e, 0x91, 0xc7, 0x78, 0x4f, 0x2e, 0x02, 0xe0, 0x4e,
0x3d, 0x12, 0x57, 0x09, 0xd9, 0x3b, 0x44, 0x0c, 0x21, 0x2d, 0x93, 0xf0, 0x70, 0x28, 0xc1, 0x4b, 0xfc, 0xf9, 0xa2,
0x98, 0x22, 0x61, 0xe5, 0xc0, 0x28, 0xc8, 0xb3, 0xe3, 0x82, 0xe5, 0xa7, 0x6c, 0xac, 0x39, 0xa0, 0x00, 0xac, 0xa8,
0x39, 0x18, 0x2f, 0x34, 0xa3, 0xa3, 0x74, 0x28, 0x83, 0xa1, 0x7a, 0xa6, 0x98, 0x65, 0x92, 0x99, 0xb5, 0x85, 0xa3,
0xa5, 0x80, 0x23, 0x8c, 0x0a, 0x29, 0x09, 0xf2, 0x50, 0x61, 0x38, 0x05, 0x29, 0x04, 0x5a, 0xc1, 0xdc, 0xe6, 0x4a,
0x93, 0x3d, 0x5b, 0x90, 0x4a, 0xc8, 0xa1, 0x23, 0x6c, 0x64, 0x82, 0x34, 0x77, 0x61, 0x57, 0x81, 0x94, 0x97, 0xe0,
0x0a, 0x29, 0xa2, 0xcc, 0x1c, 0x64, 0x80, 0xf0, 0x5b, 0xa1, 0x0b, 0x7d, 0x6c, 0x41, 0x6c, 0xe0, 0xeb, 0x95, 0x07,
0xc2, 0x4a, 0xbc, 0x2b, 0x44, 0xbc, 0x2b, 0xc0, 0xc6, 0x89, 0x91, 0x9f, 0xbc, 0x3b, 0xdc, 0x4f, 0xb3, 0xbd, 0xd1,
0x88, 0x15, 0x45, 0x06, 0xb0, 0xdd, 0xa1, 0xf6, 0x57, 0x19, 0x5a, 0x40, 0x49, 0x57, 0xcb, 0x3a, 0xbb, 0x20, 0x0d,
0x6e, 0xaa, 0x15, 0xa5, 0xd3, 0x03, 0xfb, 0xe3, 0x47, 0x90, 0xd9, 0x9e, 0x24, 0x03, 0x50, 0x7d, 0xd5, 0xf0, 0x13,
0xf6, 0x4c, 0x9d, 0x32, 0x6b, 0xed, 0x4b, 0xa7, 0x0e, 0x92, 0x07, 0xc3, 0xba, 0xa5, 0xb1, 0xa0, 0x6b, 0x87, 0xc6,
0xd5, 0x90, 0x0a, 0x72, 0x79, 0x42, 0x2a, 0xdb, 0x58, 0x46, 0xb0, 0xda, 0x4a, 0x8f, 0x48, 0xaf, 0xb0, 0x29, 0x08,
0xd0, 0x43, 0x36, 0xec, 0xc9, 0xfa, 0x30, 0x17, 0x94, 0xcb, 0xd9, 0xaf, 0x0b, 0x56, 0x70, 0xc1, 0xba, 0x30, 0x6e,
0x01, 0xe3, 0x96, 0x2b, 0xd6, 0x61, 0xcd, 0x76, 0x5c, 0x07, 0xdb, 0x9b, 0x39, 0xea, 0xb1, 0x02, 0x39, 0xf9, 0x7a,
0x76, 0x42, 0x58, 0x99, 0x7b, 0x79, 0xf9, 0xad, 0x1a, 0xa4, 0x5a, 0x4a, 0x6d, 0x03, 0x35, 0xd6, 0xc4, 0x56, 0x4d,
0xc6, 0xb6, 0x2b, 0x15, 0xea, 0x9d, 0x4e, 0xaf, 0xc6, 0x07, 0xb0, 0xe7, 0xda, 0x9a, 0xa5, 0x2b, 0x63, 0xfb, 0xad,
0xa2, 0xe9, 0x1b, 0x31, 0x32, 0x59, 0xa3, 0xec, 0x66, 0xee, 0x51, 0x3b, 0x1e, 0xda, 0xae, 0xd4, 0x55, 0x82, 0x61,
0x51, 0x17, 0x0c, 0x4d, 0xa8, 0xe7, 0xba, 0x8b, 0xad, 0x99, 0x8a, 0x85, 0x6a, 0xad, 0x95, 0x03, 0xc1, 0xc3, 0x43,
0x30, 0x4e, 0xd6, 0xfa, 0x07, 0xaf, 0xa3, 0x19, 0x43, 0x8a, 0x7a, 0x57, 0x35, 0x90, 0x0e, 0x04, 0x34, 0x19, 0x36,
0xd5, 0x1b, 0x77, 0x85, 0xd5, 0x54, 0xdf, 0x5f, 0x31, 0x58, 0x11, 0x60, 0x5f, 0x97, 0x6b, 0x96, 0x88, 0xf4, 0xa6,
0xe0, 0x12, 0x4d, 0x1f, 0x51, 0x26, 0xd6, 0x84, 0x14, 0x3c, 0x20, 0x0f, 0xcb, 0xdf, 0x58, 0x38, 0xd9, 0x8a, 0x29,
0x1c, 0x39, 0xca, 0x14, 0xa0, 0x33, 0x29, 0x01, 0x10, 0x97, 0xf4, 0xb3, 0xb6, 0xb1, 0x90, 0x6c, 0xfb, 0xc8, 0x07,
0xfe, 0x24, 0x89, 0xb8, 0xd3, 0xd9, 0x6a, 0xbb, 0xc0, 0x87, 0x20, 0xc4, 0x41, 0x47, 0x80, 0x79, 0x5f, 0xa1, 0xc2,
0x10, 0x95, 0xd8, 0xe5, 0x3e, 0x18, 0x45, 0xd3, 0x78, 0xc2, 0x9d, 0x0c, 0x95, 0x88, 0x5b, 0xb2, 0x04, 0x94, 0x8c,
0xde, 0x57, 0x20, 0x25, 0xb8, 0x90, 0x2e, 0xa2, 0x5a, 0x0b, 0x34, 0x05, 0x29, 0x49, 0x29, 0xd2, 0x82, 0x0a, 0x02,
0x43, 0xa8, 0xf4, 0x14, 0x47, 0x81, 0x7e, 0x8b, 0x07, 0x62, 0xd0, 0x60, 0xc5, 0xa2, 0x8c, 0x07, 0xf1, 0x6a, 0x21,
0xa8, 0x61, 0x9f, 0x67, 0x2f, 0xb3, 0x33, 0x96, 0x3f, 0x89, 0x10, 0xf6, 0x40, 0x74, 0x2f, 0x41, 0xd2, 0x93, 0x40,
0x67, 0x3d, 0xc5, 0x2b, 0xa7, 0x84, 0x34, 0x2c, 0xc4, 0x2c, 0x46, 0x45, 0x08, 0x5a, 0x8e, 0x68, 0x9f, 0xe2, 0x96,
0xa2, 0xbd, 0x87, 0xaa, 0x84, 0x69, 0xde, 0xda, 0x7b, 0x59, 0xe7, 0x2d, 0x18, 0x61, 0xae, 0xb8, 0xb5, 0xbe, 0x63,
0x5d, 0x4f, 0xea, 0x66, 0x47, 0xf2, 0x96, 0xa1, 0xcc, 0x40, 0x7f, 0x5c, 0x5e, 0x56, 0x46, 0x3a, 0x28, 0x53, 0x2d,
0xcd, 0xd1, 0x72, 0x12, 0x5b, 0xc2, 0x2d, 0x41, 0x19, 0xa1, 0xe1, 0x95, 0x67, 0x49, 0x62, 0xe8, 0x22, 0x2f, 0xee,
0x39, 0x0d, 0x75, 0x04, 0x50, 0xcc, 0x6a, 0x1a, 0x69, 0xc0, 0x03, 0x5d, 0x81, 0x4a, 0x49, 0x69, 0x23, 0xaf, 0x6a,
0x22, 0x20, 0x4e, 0xc7, 0x2c, 0x17, 0x0e, 0x9a, 0xd4, 0xa1, 0x30, 0x61, 0x0a, 0x0c, 0xcd, 0xc6, 0x20, 0xe1, 0x15,
0x02, 0x60, 0x9e, 0xf8, 0xd3, 0xac, 0xe0, 0xba, 0xce, 0x84, 0x3e, 0xbe, 0xbc, 0x8c, 0x85, 0xbf, 0x88, 0x0c, 0x90,
0xb3, 0x59, 0x76, 0xca, 0xd6, 0x40, 0xdd, 0x53, 0x83, 0x99, 0x20, 0x1b, 0xc3, 0x80, 0x12, 0x05, 0xd5, 0x32, 0x4f,
0xe2, 0x11, 0xd3, 0x5a, 0x6a, 0xe6, 0x83, 0x41, 0xc7, 0xce, 0x41, 0x46, 0x30, 0xb7, 0xdf, 0xef, 0xb7, 0xbd, 0x8e,
0x5b, 0x0a, 0x82, 0x2f, 0x57, 0x28, 0x7a, 0x8d, 0x7e, 0x94, 0x26, 0xf8, 0x3a, 0x59, 0xc0, 0x5d, 0x43, 0x29, 0x72,
0xe1, 0x27, 0x79, 0x52, 0x10, 0xbb, 0xde, 0x18, 0x06, 0xe5, 0x4c, 0x09, 0x6e, 0x34, 0x71, 0xc5, 0xb6, 0x7d, 0xa7,
0xc9, 0xa6, 0xd9, 0x49, 0xed, 0x30, 0xb5, 0x30, 0x72, 0xcd, 0x0b, 0xed, 0x01, 0x9b, 0xcb, 0x83, 0x56, 0x22, 0x55,
0x03, 0xaf, 0x03, 0x84, 0xc2, 0xd3, 0x75, 0x56, 0x50, 0xaa, 0x3a, 0x4b, 0x21, 0xae, 0x37, 0xd0, 0x5b, 0x26, 0xc1,
0x5c, 0x47, 0x82, 0x7d, 0x29, 0x10, 0x38, 0x7a, 0x64, 0x62, 0xbd, 0x9e, 0xc0, 0xf2, 0x1c, 0x47, 0xa3, 0x4f, 0x1a,
0xdc, 0x8a, 0xec, 0x4d, 0x36, 0x70, 0x1a, 0x25, 0xa1, 0x21, 0xae, 0x4c, 0xbc, 0x95, 0x84, 0xae, 0x6d, 0x14, 0x70,
0xc8, 0x56, 0xd8, 0xbe, 0xb9, 0xd0, 0x4d, 0x6e, 0x97, 0xec, 0xa1, 0xfc, 0x27, 0xcd, 0x25, 0xd7, 0xb0, 0x1c, 0x57,
0xd2, 0x80, 0x2b, 0xc6, 0x83, 0xa5, 0x69, 0x40, 0x02, 0x7c, 0x57, 0x8e, 0xe3, 0xe2, 0x6a, 0x12, 0xfc, 0xa1, 0x60,
0x3e, 0x35, 0x66, 0xba, 0x11, 0x52, 0x2d, 0xe1, 0xa4, 0x19, 0xac, 0x41, 0x93, 0xc6, 0x83, 0x12, 0x35, 0xdf, 0xa2,
0xa1, 0x42, 0x1c, 0x7f, 0x22, 0xaa, 0xd0, 0x04, 0x43, 0x30, 0x72, 0xaf, 0x90, 0x0c, 0x97, 0xad, 0x8a, 0x16, 0x29,
0x53, 0x63, 0x52, 0xa9, 0x9a, 0xe5, 0x32, 0x30, 0xb0, 0x68, 0xb7, 0xfa, 0xd2, 0x12, 0x57, 0x22, 0x37, 0x0d, 0xb5,
0x30, 0x29, 0x94, 0x37, 0xe1, 0xe4, 0xe8, 0x77, 0x29, 0xeb, 0xdd, 0xc4, 0x27, 0x57, 0xf8, 0xe4, 0xbe, 0xe1, 0x43,
0x99, 0xbc, 0x5d, 0x0c, 0x8a, 0xe0, 0x9b, 0x5a, 0x25, 0xda, 0xa7, 0x3e, 0x0a, 0x66, 0x57, 0x0b, 0x5d, 0x10, 0x28,
0x92, 0x4d, 0xd2, 0x81, 0xe4, 0x37, 0x14, 0x1b, 0x95, 0x67, 0x94, 0xb9, 0x62, 0x83, 0xd4, 0xbc, 0xd2, 0xcc, 0x4b,
0xdd, 0x86, 0xfd, 0x5e, 0x96, 0x92, 0x4e, 0x5c, 0x50, 0x26, 0xf6, 0xae, 0xa3, 0x8d, 0x97, 0x86, 0x99, 0xb0, 0x7e,
0x85, 0xb1, 0x53, 0xa3, 0x50, 0x2a, 0x45, 0x20, 0x8e, 0x8d, 0xaf, 0x95, 0x65, 0x90, 0xf9, 0x6b, 0xec, 0x29, 0x00,
0x25, 0x81, 0xc5, 0xd7, 0x54, 0xf2, 0xa2, 0xb0, 0x4e, 0xc7, 0x3b, 0x44, 0xc7, 0x4a, 0x84, 0xd6, 0x44, 0xbe, 0xd6,
0x67, 0xb1, 0x5f, 0x73, 0x09, 0x4d, 0x4a, 0xe6, 0x83, 0x3c, 0xb0, 0x55, 0x20, 0xa2, 0xd2, 0x6d, 0xc9, 0x20, 0x21,
0x87, 0x74, 0x95, 0xe8, 0xb5, 0x91, 0x0c, 0x5a, 0xa7, 0x42, 0xa2, 0xa5, 0xc3, 0x30, 0x72, 0xd0, 0x71, 0xa7, 0xb5,
0x58, 0x21, 0x64, 0xd3, 0xde, 0x24, 0x56, 0x44, 0xe7, 0x34, 0x47, 0x13, 0xce, 0xd4, 0xe9, 0x8e, 0x03, 0xe8, 0x80,
0xd8, 0x5f, 0x61, 0xbd, 0xb5, 0x66, 0xa7, 0xeb, 0x57, 0x0e, 0xdf, 0xe5, 0x65, 0x82, 0xfc, 0x20, 0x0c, 0x5e, 0x58,
0xb3, 0x81, 0x92, 0xbd, 0x7b, 0x2f, 0xb1, 0x15, 0xd9, 0x9f, 0x55, 0x49, 0xe5, 0x29, 0xd4, 0x38, 0xb7, 0xbe, 0x4e,
0xcc, 0x0c, 0x2d, 0xaa, 0x8a, 0x7d, 0x43, 0xaa, 0xef, 0x2b, 0x85, 0x5d, 0xa1, 0xbc, 0x2f, 0x87, 0x8e, 0x5d, 0xd7,
0x0d, 0x72, 0x72, 0x5e, 0xee, 0xac, 0x73, 0x21, 0xef, 0xde, 0x35, 0x7d, 0xa6, 0x53, 0x3d, 0xfc, 0x13, 0x07, 0x95,
0x73, 0x71, 0x91, 0x92, 0x05, 0xf3, 0x44, 0xa9, 0xa3, 0x15, 0x07, 0xb4, 0xdd, 0x43, 0x4f, 0x3b, 0x3a, 0x8b, 0x62,
0x6e, 0xe9, 0x51, 0x84, 0xa7, 0x8d, 0xf2, 0x49, 0x1a, 0x1d, 0x80, 0x17, 0x9a, 0x90, 0xe4, 0x84, 0x9b, 0xb6, 0x68,
0x31, 0x9a, 0x32, 0x0c, 0x81, 0x2b, 0x7b, 0xc2, 0x94, 0x3d, 0x77, 0x10, 0x6f, 0x31, 0x30, 0x5b, 0x0f, 0x7b, 0xd9,
0xec, 0x5e, 0x33, 0xff, 0x61, 0x8d, 0x40, 0xb6, 0xcd, 0x54, 0x5d, 0xd9, 0x78, 0x97, 0x22, 0x12, 0x23, 0x6c, 0xeb,
0xc6, 0x96, 0xb6, 0x7e, 0xaf, 0xe1, 0x5e, 0x57, 0x8e, 0x79, 0x4d, 0xa9, 0x36, 0xf4, 0xb0, 0x72, 0x73, 0x98, 0xe9,
0xc8, 0x8b, 0x15, 0x74, 0x7b, 0x22, 0x28, 0x04, 0x4e, 0x84, 0xb6, 0x07, 0x15, 0x37, 0x10, 0x29, 0xb9, 0xd2, 0xaa,
0xd9, 0x22, 0x19, 0x4b, 0x60, 0xc1, 0x85, 0xe5, 0x92, 0x8f, 0xce, 0xe2, 0x24, 0xa9, 0x4a, 0xff, 0x50, 0x01, 0x2f,
0x86, 0xbd, 0x49, 0xb4, 0x0b, 0x8c, 0x16, 0x0a, 0x04, 0x57, 0x1b, 0x61, 0xef, 0x1d, 0xb7, 0x5a, 0x77, 0x11, 0x71,
0xe4, 0x66, 0x34, 0x02, 0xea, 0x31, 0xc2, 0xaa, 0x59, 0x7b, 0xef, 0x19, 0x86, 0xd4, 0x0c, 0x7c, 0x50, 0x9d, 0x51,
0xf1, 0x67, 0xd9, 0x53, 0x9f, 0x89, 0xde, 0x8d, 0xaa, 0xab, 0x19, 0x50, 0x51, 0x81, 0x0f, 0x33, 0xc4, 0xd2, 0x56,
0x81, 0x80, 0x5c, 0x0f, 0x8b, 0x52, 0xc0, 0x24, 0x0d, 0x16, 0x94, 0x02, 0x6b, 0xad, 0xec, 0x5e, 0xde, 0x14, 0xcc,
0xa1, 0x50, 0xb8, 0xe8, 0xff, 0x24, 0x9b, 0xcd, 0xd1, 0x32, 0x6b, 0x30, 0x35, 0x34, 0x78, 0xdf, 0xa8, 0x2f, 0xd7,
0x94, 0xd5, 0xfa, 0xd0, 0x8e, 0xac, 0xf1, 0x93, 0x76, 0x94, 0xc1, 0xa1, 0x5a, 0xe8, 0xa2, 0xba, 0xdd, 0xdc, 0x14,
0x31, 0xeb, 0x78, 0xdc, 0x27, 0xbd, 0xad, 0xad, 0x49, 0x4f, 0xd3, 0x80, 0x64, 0x92, 0x64, 0x78, 0x93, 0x01, 0xca,
0x8a, 0x38, 0xcb, 0xb2, 0x41, 0xbe, 0x65, 0x59, 0xe2, 0xfa, 0x7d, 0xd7, 0xdb, 0xab, 0x79, 0xd6, 0xde, 0xde, 0xd5,
0x2e, 0x72, 0x55, 0x27, 0x3d, 0xc8, 0xc3, 0x21, 0x14, 0xad, 0xd8, 0x94, 0xe1, 0x72, 0x96, 0x8d, 0x59, 0x60, 0x43,
0xf7, 0xd4, 0x2e, 0x95, 0x56, 0x86, 0xcd, 0x91, 0x32, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4,
0xb9, 0xf0, 0x7d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62,
0x35, 0x94, 0xd4, 0xf7, 0x83, 0xef, 0x83, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11,
0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x8b, 0x58, 0xf8, 0x72, 0xf0, 0x52,
0xb9, 0xd4, 0x19, 0xb8, 0x10, 0xe0, 0xe4, 0x27, 0x31, 0x6f, 0xe1, 0x79, 0xa4, 0xed, 0x2d, 0x45, 0x05, 0xc6, 0x15,
0x29, 0x2e, 0x5d, 0x2a, 0x6f, 0xd0, 0xfb, 0x18, 0x1e, 0x41, 0xb3, 0x8d, 0x8d, 0xa5, 0xf3, 0x2a, 0xe2, 0x53, 0x3f,
0x8f, 0xd2, 0x71, 0x36, 0x73, 0xdc, 0x4d, 0xdb, 0x76, 0xfd, 0x82, 0x3c, 0x91, 0xaf, 0xdc, 0x72, 0xe3, 0xc8, 0x9b,
0xb2, 0xd0, 0x1e, 0xd8, 0x9b, 0x1f, 0xbd, 0xf7, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x4e, 0x59, 0xd9, 0x3f, 0xf2, 0xce,
0x9c, 0xd3, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x57, 0x20, 0x44, 0x52, 0x96, 0xed, 0x02, 0x05, 0xf2, 0xca,
0x4b, 0x5d, 0xbb, 0xca, 0x5b, 0x59, 0xb2, 0x6b, 0x51, 0xb1, 0x2c, 0x88, 0x4c, 0x8a, 0x28, 0x83, 0x00, 0x0b, 0x48,
0x6a, 0x29, 0x0a, 0x7d, 0xfa, 0xa9, 0x9f, 0xe6, 0x9c, 0x59, 0x1f, 0xfa, 0x65, 0x4e, 0xf7, 0xc3, 0x7c, 0xc4, 0x3c,
0xf7, 0xa7, 0xdc, 0x1f, 0x98, 0xfe, 0x84, 0x89, 0x88, 0x5c, 0x90, 0x00, 0xa9, 0xc5, 0xd5, 0xd5, 0x7d, 0xbc, 0x08,
0xc8, 0x35, 0x22, 0x32, 0x32, 0xb6, 0x8c, 0x84, 0x76, 0xef, 0x8c, 0xb3, 0x11, 0xbf, 0x98, 0x33, 0x6b, 0xca, 0x67,
0x49, 0x7f, 0x57, 0xfe, 0xcf, 0xa2, 0x71, 0x7f, 0x37, 0x89, 0xd3, 0x4f, 0x56, 0xce, 0x92, 0x30, 0x1e, 0x65, 0xa9,
0x35, 0xcd, 0xd9, 0x24, 0x1c, 0x47, 0x3c, 0x0a, 0xe2, 0x59, 0x74, 0xc2, 0xac, 0xad, 0xfe, 0xee, 0x8c, 0xf1, 0xc8,
0x1a, 0x4d, 0xa3, 0xbc, 0x60, 0x3c, 0x7c, 0x7f, 0xf0, 0x55, 0xeb, 0x51, 0x7f, 0xb7, 0x18, 0xe5, 0xf1, 0x9c, 0x5b,
0x38, 0x64, 0x38, 0xcb, 0xc6, 0x8b, 0x84, 0xf5, 0x4f, 0xa3, 0xdc, 0x7a, 0xc6, 0xc2, 0x37, 0xc7, 0xbf, 0xb0, 0x11,
0xf7, 0xc7, 0x6c, 0x12, 0xa7, 0xec, 0x6d, 0x9e, 0xcd, 0x59, 0xce, 0x2f, 0xbc, 0xfd, 0xf5, 0x15, 0x31, 0x2b, 0xbc,
0x4f, 0xba, 0xea, 0x84, 0xf1, 0x37, 0x67, 0xa9, 0xea, 0xf3, 0x94, 0x89, 0x49, 0xb2, 0xbc, 0xf0, 0x8a, 0x2b, 0xda,
0xec, 0x5f, 0xcc, 0x8e, 0xb3, 0xa4, 0xf0, 0x9e, 0xe8, 0xfa, 0x79, 0x9e, 0xf1, 0x0c, 0xc1, 0xf2, 0xa7, 0x51, 0x61,
0xb4, 0xf4, 0xde, 0xac, 0x69, 0x32, 0x97, 0x95, 0x2f, 0x8a, 0x67, 0xe9, 0x62, 0xc6, 0xf2, 0xe8, 0x38, 0x61, 0x5e,
0xcc, 0x42, 0x87, 0x79, 0xdc, 0x8b, 0xdd, 0xb0, 0xcf, 0xad, 0x38, 0xb5, 0xd8, 0xe0, 0x19, 0xa3, 0x92, 0x25, 0xd3,
0xad, 0x82, 0x3b, 0x6d, 0x0f, 0xc8, 0x35, 0x89, 0x4f, 0x16, 0xfa, 0xfd, 0x2c, 0x8f, 0xb9, 0x7a, 0x3e, 0x8d, 0x92,
0x05, 0x0b, 0xe2, 0xd2, 0x0d, 0xd8, 0x21, 0x1f, 0x86, 0xb1, 0xf7, 0x84, 0x06, 0x85, 0x21, 0x97, 0x93, 0x2c, 0x77,
0x90, 0x56, 0x31, 0x8e, 0xcd, 0x2f, 0x2f, 0x1d, 0x1e, 0x2e, 0x4b, 0xd7, 0x7d, 0xc2, 0xfc, 0x51, 0x94, 0x24, 0x0e,
0x4e, 0x7c, 0xf7, 0x6e, 0x8c, 0x33, 0xc6, 0x1e, 0x3f, 0x8c, 0x87, 0x6e, 0x2f, 0x9e, 0x38, 0x05, 0x73, 0xab, 0x7e,
0xd9, 0xc4, 0x2a, 0x98, 0xc3, 0x5d, 0xf7, 0xcd, 0xd5, 0x7d, 0x72, 0xc6, 0x17, 0x39, 0xc0, 0x5e, 0x7a, 0x6f, 0xd4,
0xcc, 0xfb, 0x58, 0xff, 0x89, 0x3a, 0xf6, 0x00, 0xf6, 0x82, 0x5b, 0x1f, 0xc2, 0xb3, 0x38, 0x1d, 0x67, 0x67, 0xfe,
0xfe, 0x34, 0x82, 0x1f, 0xef, 0xb2, 0x8c, 0xdf, 0xbd, 0xeb, 0x9c, 0x66, 0xf1, 0xd8, 0x6a, 0x87, 0xa1, 0x59, 0x79,
0xf1, 0x64, 0x7f, 0xff, 0xf2, 0xb2, 0x51, 0xe0, 0xa7, 0x11, 0x8f, 0x4f, 0x99, 0xe8, 0x0c, 0x00, 0xd8, 0xf0, 0x73,
0xce, 0xd9, 0x78, 0x9f, 0x5f, 0x24, 0x50, 0xca, 0x18, 0x2f, 0x6c, 0xc0, 0xf1, 0x69, 0x36, 0x02, 0xb2, 0xa5, 0x06,
0xe1, 0xa1, 0x69, 0xce, 0xe6, 0x49, 0x34, 0x62, 0x58, 0x0f, 0x23, 0x55, 0x3d, 0xaa, 0x46, 0xde, 0x57, 0xa1, 0x58,
0x5e, 0xc7, 0xf5, 0x72, 0x16, 0xa6, 0xec, 0xcc, 0x7a, 0x15, 0xcd, 0x7b, 0xa3, 0x24, 0x2a, 0x0a, 0x2b, 0x63, 0x4b,
0x42, 0x21, 0x5f, 0x8c, 0x80, 0x41, 0x08, 0xc1, 0x25, 0x90, 0x89, 0x4f, 0xe3, 0xc2, 0xff, 0xb8, 0x31, 0x2a, 0x8a,
0x77, 0xac, 0x58, 0x24, 0x7c, 0x23, 0x84, 0xb5, 0xe0, 0x77, 0xc2, 0xf0, 0x2b, 0x97, 0x4f, 0xf3, 0xec, 0xcc, 0x7a,
0x96, 0xe7, 0xd0, 0xdc, 0x86, 0x29, 0x45, 0x03, 0x2b, 0x2e, 0xac, 0x34, 0xe3, 0x96, 0x1e, 0x0c, 0x17, 0xd0, 0xb7,
0xde, 0x17, 0xcc, 0x3a, 0x5a, 0xa4, 0x45, 0x34, 0x61, 0xd0, 0xf4, 0xc8, 0xca, 0x72, 0xeb, 0x08, 0x06, 0x3d, 0x82,
0x25, 0x2b, 0x38, 0xec, 0x1a, 0xdf, 0x76, 0x7b, 0x34, 0x17, 0x14, 0x1e, 0xb0, 0x73, 0x1e, 0xb2, 0x12, 0x18, 0xd3,
0x2a, 0x34, 0x1a, 0x8e, 0xbb, 0x4c, 0xa0, 0x80, 0x85, 0x39, 0x43, 0x96, 0x75, 0xcc, 0xc6, 0x7a, 0x71, 0x3e, 0xdc,
0xbd, 0xab, 0x69, 0x0d, 0x34, 0x71, 0xa0, 0x6d, 0xd1, 0x68, 0xeb, 0x09, 0xc4, 0x6b, 0x24, 0x72, 0x3d, 0xe6, 0x4b,
0xf2, 0xed, 0x5f, 0xa4, 0xa3, 0xfa, 0xd8, 0x50, 0x59, 0xf2, 0x6c, 0x9f, 0xe7, 0x71, 0x7a, 0x02, 0x40, 0xc8, 0x99,
0xcc, 0x26, 0x65, 0x29, 0x16, 0xff, 0x2d, 0x0b, 0x59, 0xd8, 0xc7, 0xd1, 0x33, 0xe6, 0xd8, 0x05, 0xf5, 0xb0, 0xc3,
0x10, 0x49, 0x0f, 0x0c, 0xc6, 0x06, 0x2c, 0x60, 0x9b, 0xb6, 0xed, 0x7d, 0xe5, 0x7a, 0x17, 0xc8, 0x41, 0xbe, 0xef,
0x13, 0xfb, 0x8a, 0xce, 0x71, 0xd8, 0x41, 0xa0, 0xfd, 0x84, 0xa5, 0x27, 0x7c, 0x3a, 0x60, 0x87, 0xed, 0x61, 0xc0,
0x01, 0xaa, 0xf1, 0x62, 0xc4, 0x1c, 0xe4, 0x47, 0x2f, 0xc7, 0xed, 0xb3, 0xe9, 0xc0, 0x14, 0xb8, 0x30, 0x77, 0x08,
0xc7, 0xda, 0xd2, 0xb8, 0x8a, 0x45, 0x15, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x63, 0x96, 0x1b, 0x70, 0xe8, 0x66,
0xbd, 0xda, 0x0a, 0x2e, 0x60, 0x85, 0xa0, 0x9f, 0x35, 0x59, 0xa4, 0x23, 0x1e, 0x83, 0xe0, 0xb2, 0x37, 0x01, 0x5c,
0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0x64, 0x87, 0xf9, 0x66, 0x67, 0xe8, 0x21, 0x94, 0x9a,
0xf8, 0x12, 0xf1, 0x18, 0x10, 0x2c, 0xbd, 0xf7, 0x4c, 0x6f, 0xcf, 0x0f, 0x03, 0xe6, 0xaf, 0xf2, 0x71, 0xc8, 0xfd,
0x59, 0x34, 0x47, 0x6c, 0x18, 0xf1, 0x40, 0x94, 0x8e, 0x10, 0xba, 0xda, 0xba, 0x20, 0xc5, 0xfc, 0x8a, 0x05, 0x5c,
0x20, 0x08, 0xec, 0xd9, 0x67, 0xd1, 0x68, 0x0a, 0x5b, 0xbc, 0x22, 0xdc, 0x58, 0x6d, 0x87, 0x51, 0xce, 0x22, 0xce,
0x9e, 0x25, 0x0c, 0xdf, 0x70, 0x05, 0xa0, 0xa7, 0x0d, 0xbc, 0xae, 0xf6, 0x5d, 0x12, 0xf3, 0xd7, 0x19, 0xcc, 0xd3,
0x13, 0x4c, 0x02, 0x5c, 0x9c, 0xc3, 0x26, 0x47, 0x16, 0xd9, 0xe3, 0xb0, 0x5a, 0xc7, 0x0b, 0x0e, 0xeb, 0x96, 0x62,
0x0b, 0x1b, 0xa8, 0xed, 0xc5, 0x3e, 0x07, 0x22, 0x3e, 0xc9, 0x52, 0x0e, 0xc3, 0x01, 0xbc, 0x9a, 0x83, 0xfc, 0x68,
0x3e, 0x67, 0xe9, 0xf8, 0xc9, 0x34, 0x4e, 0xc6, 0x40, 0x8d, 0x12, 0xf0, 0x4d, 0x59, 0x08, 0x78, 0x02, 0x32, 0xc1,
0xf5, 0x18, 0xd1, 0xf2, 0x21, 0x23, 0xf3, 0xd0, 0xb6, 0x7b, 0x28, 0x81, 0x24, 0x16, 0x28, 0x83, 0x68, 0xe1, 0xde,
0x81, 0xe8, 0x2f, 0x5c, 0xbe, 0x19, 0xc6, 0x7a, 0x19, 0x25, 0x81, 0xdf, 0xa2, 0xa4, 0x01, 0xfa, 0x33, 0x90, 0x81,
0x3d, 0x14, 0x5c, 0xdf, 0x49, 0xa9, 0x93, 0x30, 0x85, 0x21, 0x10, 0x60, 0x84, 0x12, 0x44, 0xd2, 0xe0, 0x6d, 0x96,
0x5c, 0x4c, 0xe2, 0x24, 0xd9, 0x5f, 0xcc, 0xe7, 0x59, 0xce, 0xbd, 0xaf, 0xc3, 0x25, 0xcf, 0x2a, 0x5c, 0x69, 0x93,
0x17, 0x67, 0x31, 0x47, 0x82, 0xba, 0xcb, 0x51, 0x04, 0x4b, 0xfd, 0x38, 0xcb, 0x12, 0x16, 0xa5, 0x80, 0x06, 0x1b,
0xd8, 0x76, 0x90, 0x2e, 0x92, 0xa4, 0x77, 0x0c, 0xc3, 0x7e, 0xea, 0x51, 0xb5, 0x90, 0xf8, 0x01, 0x3d, 0xef, 0xe5,
0x79, 0x74, 0x01, 0x0d, 0xb1, 0x0d, 0xf0, 0x22, 0xac, 0xd6, 0xd7, 0xfb, 0x6f, 0x5e, 0xfb, 0x82, 0xf1, 0xe3, 0xc9,
0x05, 0x00, 0x5a, 0x56, 0x52, 0x73, 0x92, 0x67, 0xb3, 0xc6, 0xd4, 0x48, 0x87, 0x38, 0x64, 0xbd, 0x2b, 0x40, 0x88,
0x69, 0x64, 0x58, 0x25, 0x66, 0x42, 0xf0, 0x9a, 0xf8, 0x59, 0x56, 0xe2, 0x1e, 0x18, 0xe0, 0x43, 0x20, 0x8a, 0x61,
0xca, 0xeb, 0xa1, 0xe5, 0xf9, 0xc5, 0x32, 0x0e, 0x09, 0xce, 0x39, 0xea, 0x5f, 0x84, 0x71, 0x14, 0xc1, 0xec, 0x4b,
0x31, 0x60, 0xa9, 0x20, 0x8e, 0xcb, 0xd2, 0x8b, 0x34, 0x13, 0xa3, 0xc4, 0x43, 0x81, 0xc2, 0x61, 0x1b, 0x5d, 0x5e,
0x32, 0x78, 0x71, 0xbd, 0x6f, 0xc2, 0x65, 0xa4, 0xf0, 0x41, 0x0d, 0x85, 0xfb, 0x2b, 0x10, 0x72, 0x02, 0x35, 0xd9,
0x29, 0xe8, 0x41, 0x80, 0xf3, 0x6b, 0x10, 0xb5, 0x93, 0x04, 0xa1, 0xb8, 0xd3, 0xf1, 0x40, 0x83, 0x3e, 0x99, 0x46,
0xe9, 0x09, 0x1b, 0x07, 0x11, 0x2b, 0xa5, 0xe4, 0xdd, 0xb3, 0x60, 0x8d, 0x81, 0x9d, 0x0a, 0xeb, 0xf9, 0xc1, 0xab,
0x97, 0x72, 0xe5, 0x6a, 0xc2, 0x18, 0x16, 0x69, 0x01, 0x6a, 0x15, 0xc4, 0xb6, 0x14, 0xc7, 0xcf, 0xb8, 0x92, 0xde,
0xa2, 0x24, 0x2e, 0xde, 0xcf, 0xc1, 0xc4, 0x60, 0x6f, 0x61, 0x18, 0x98, 0x3e, 0x84, 0xa9, 0xa8, 0x1c, 0xe6, 0x13,
0x15, 0x63, 0x5d, 0x04, 0x9d, 0x05, 0xa6, 0xe2, 0x35, 0x73, 0xdc, 0x12, 0x58, 0x95, 0xc7, 0x23, 0x2b, 0x1a, 0x8f,
0x5f, 0xa4, 0x31, 0x8f, 0xa3, 0x24, 0xfe, 0x8d, 0x28, 0xb9, 0x44, 0x1e, 0xe3, 0x3d, 0xb9, 0x08, 0x80, 0x3b, 0xf5,
0x48, 0x5c, 0x25, 0x64, 0xef, 0x10, 0x31, 0x84, 0xb4, 0x4c, 0xc2, 0xc3, 0xa1, 0x04, 0x2f, 0xf1, 0xe7, 0x8b, 0x62,
0x8a, 0x84, 0x95, 0x03, 0xa3, 0x20, 0xcf, 0x8e, 0x0b, 0x96, 0x9f, 0xb2, 0xb1, 0xe6, 0x80, 0x02, 0xb0, 0xa2, 0xe6,
0x60, 0xbc, 0xd0, 0x8c, 0x8e, 0xd2, 0xa1, 0x0c, 0x86, 0xea, 0x99, 0x62, 0x96, 0x49, 0x66, 0xd6, 0x16, 0x8e, 0x96,
0x02, 0x8e, 0x30, 0x2a, 0xa4, 0x24, 0xc8, 0x43, 0x85, 0xe1, 0x14, 0xa4, 0x10, 0x68, 0x05, 0x73, 0x9b, 0x2b, 0x4d,
0xf6, 0x6c, 0x41, 0x2a, 0x21, 0x87, 0x8e, 0xb0, 0x91, 0x09, 0xd2, 0xdc, 0x85, 0x5d, 0x05, 0x52, 0x5e, 0x82, 0x2b,
0xa4, 0x88, 0x32, 0x73, 0x90, 0x01, 0xc2, 0x6f, 0x84, 0x2e, 0xf4, 0xb1, 0x05, 0xb1, 0x81, 0xaf, 0x57, 0x1e, 0x08,
0x2b, 0xf1, 0xae, 0x10, 0xf1, 0xae, 0x00, 0x1b, 0x27, 0x46, 0x7e, 0xf2, 0xee, 0x70, 0x3f, 0xcd, 0xf6, 0x46, 0x23,
0x56, 0x14, 0x19, 0xc0, 0x76, 0x87, 0xda, 0x5f, 0x65, 0x68, 0x01, 0x25, 0x5d, 0x2d, 0xeb, 0xec, 0x82, 0x34, 0xb8,
0xa9, 0x56, 0x94, 0x4e, 0x0f, 0xec, 0x8f, 0x1f, 0x41, 0x66, 0x7b, 0x92, 0x0c, 0x40, 0xf5, 0x55, 0xc3, 0x4f, 0xd8,
0x33, 0x75, 0xca, 0xac, 0xb5, 0x2f, 0x9d, 0x3a, 0x48, 0x1e, 0x0c, 0xeb, 0x96, 0xc6, 0x82, 0xae, 0x1d, 0x1a, 0x57,
0x43, 0x2a, 0xc8, 0xe5, 0x09, 0xa9, 0x6c, 0x63, 0x19, 0xc1, 0x6a, 0x2b, 0x3d, 0x22, 0xbd, 0xc2, 0xa6, 0x20, 0x40,
0x0f, 0xd9, 0xb0, 0x27, 0xeb, 0xc3, 0x5c, 0x50, 0x2e, 0x67, 0xbf, 0x2e, 0x58, 0xc1, 0x05, 0xeb, 0xc2, 0xb8, 0x05,
0x8c, 0x5b, 0xae, 0x58, 0x87, 0x35, 0xdb, 0x71, 0x1d, 0x6c, 0x6f, 0xe6, 0xa8, 0xc7, 0x0a, 0xe4, 0xe4, 0xeb, 0xd9,
0x09, 0x61, 0x65, 0xee, 0xe5, 0xe5, 0x37, 0x6a, 0x90, 0x6a, 0x29, 0xb5, 0x0d, 0xd4, 0x58, 0x13, 0x5b, 0x35, 0x19,
0xdb, 0xae, 0x54, 0xa8, 0x77, 0x3a, 0xbd, 0x1a, 0x1f, 0xc0, 0x9e, 0x6b, 0x6b, 0x96, 0xae, 0x8c, 0xed, 0xb7, 0x8a,
0xa6, 0x6f, 0xc4, 0xc8, 0x64, 0x8d, 0xb2, 0x9b, 0xb9, 0x47, 0xed, 0x78, 0x68, 0xbb, 0x52, 0x57, 0x09, 0x86, 0x45,
0x5d, 0x30, 0x34, 0xa1, 0x9e, 0xeb, 0x2e, 0xb6, 0x66, 0x2a, 0x16, 0xaa, 0xb5, 0x56, 0x0e, 0x04, 0x0f, 0x0f, 0xc1,
0x38, 0x59, 0xeb, 0x1f, 0xbc, 0x8e, 0x66, 0x0c, 0x29, 0xea, 0x5d, 0xd5, 0x40, 0x3a, 0x10, 0xd0, 0x64, 0xd8, 0x54,
0x6f, 0xdc, 0x15, 0x56, 0x53, 0x7d, 0x7f, 0xc5, 0x60, 0x45, 0x80, 0x7d, 0x5d, 0xae, 0x59, 0x22, 0xd2, 0x9b, 0x82,
0x4b, 0x34, 0x7d, 0x44, 0x99, 0x58, 0x13, 0x52, 0xf0, 0x80, 0x3c, 0x2c, 0x7f, 0x63, 0xe1, 0x64, 0x2b, 0xa6, 0x70,
0xe4, 0x28, 0x53, 0x80, 0xce, 0xa4, 0x04, 0x40, 0x5c, 0xd2, 0xcf, 0xda, 0xc6, 0x42, 0xb2, 0xed, 0x23, 0x1f, 0xf8,
0x93, 0x24, 0xe2, 0x4e, 0x67, 0xab, 0xed, 0x02, 0x1f, 0x82, 0x10, 0x07, 0x1d, 0x01, 0xe6, 0x7d, 0x85, 0x0a, 0x43,
0x54, 0x62, 0x97, 0xfb, 0x60, 0x14, 0x4d, 0xe3, 0x09, 0x77, 0x52, 0x54, 0x22, 0x6e, 0xc9, 0x12, 0x50, 0x32, 0x7a,
0x5f, 0x81, 0x94, 0xe0, 0x42, 0xba, 0x88, 0x6a, 0x2d, 0xd0, 0x14, 0xa4, 0x24, 0xa5, 0x48, 0x0b, 0x2a, 0x08, 0x0c,
0xa1, 0xd2, 0x53, 0x1c, 0x05, 0xfa, 0x2d, 0x1e, 0x88, 0x41, 0x83, 0x15, 0x8b, 0x32, 0x1e, 0xc4, 0xab, 0x85, 0xa0,
0x86, 0x7d, 0x9e, 0xbd, 0xcc, 0xce, 0x58, 0xfe, 0x24, 0x42, 0xd8, 0x03, 0xd1, 0xbd, 0x04, 0x49, 0x4f, 0x02, 0x9d,
0xf5, 0x14, 0xaf, 0x9c, 0x12, 0xd2, 0xb0, 0x10, 0xb3, 0x18, 0x15, 0x21, 0x68, 0x39, 0xa2, 0x7d, 0x8a, 0x5b, 0x8a,
0xf6, 0x1e, 0xaa, 0x12, 0xa6, 0x79, 0x6b, 0xef, 0x65, 0x9d, 0xb7, 0x60, 0x84, 0xb9, 0xe2, 0xd6, 0xfa, 0x8e, 0x75,
0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x79, 0x59, 0x19, 0xe9, 0xa0, 0x4c, 0xb5, 0x34,
0x47, 0xcb, 0x49, 0x6c, 0x09, 0xb7, 0x04, 0x65, 0x84, 0x86, 0x57, 0x9e, 0x25, 0x89, 0xa1, 0x8b, 0xbc, 0xb8, 0xe7,
0x34, 0xd4, 0x11, 0x40, 0x31, 0xab, 0x69, 0xa4, 0x01, 0x0f, 0x74, 0x05, 0x2a, 0x25, 0xa5, 0x8d, 0xbc, 0xaa, 0x89,
0x80, 0x38, 0x1d, 0xb3, 0x5c, 0x38, 0x68, 0x52, 0x87, 0xc2, 0x84, 0x29, 0x30, 0x34, 0x1b, 0x83, 0x84, 0x57, 0x08,
0x80, 0x79, 0xe2, 0x4f, 0xb3, 0x82, 0xeb, 0x3a, 0x13, 0xfa, 0xf8, 0xf2, 0x32, 0x16, 0xfe, 0x22, 0x32, 0x40, 0xce,
0x66, 0xd9, 0x29, 0x5b, 0x03, 0x75, 0x4f, 0x0d, 0x66, 0x82, 0x6c, 0x0c, 0x03, 0x4a, 0x14, 0x54, 0xcb, 0x3c, 0x89,
0x47, 0x4c, 0x6b, 0xa9, 0x99, 0x0f, 0x06, 0x1d, 0x3b, 0x07, 0x19, 0xc1, 0xdc, 0x7e, 0xbf, 0xdf, 0xf6, 0x3a, 0x6e,
0x29, 0x08, 0xbe, 0x5c, 0xa1, 0xe8, 0x35, 0xfa, 0x51, 0x9a, 0xe0, 0xeb, 0x64, 0x01, 0x77, 0x0d, 0xa5, 0xc8, 0x85,
0x9f, 0xe4, 0x49, 0x41, 0xec, 0x7a, 0x63, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, 0xdb, 0xf6, 0x9d, 0x26,
0x9b, 0x66, 0x27, 0xb5, 0xc3, 0xd4, 0xc2, 0xc8, 0x35, 0x2f, 0xb4, 0x07, 0x6c, 0x2e, 0x0f, 0x5a, 0x89, 0x54, 0x0d,
0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x41, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, 0xef, 0x99, 0x04, 0x73,
0x1d, 0x09, 0xf6, 0xa5, 0x40, 0xe0, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x02, 0xcb, 0x73, 0x1c, 0x8d, 0x3e, 0x69, 0x70,
0x2b, 0xb2, 0x37, 0xd9, 0xc0, 0x69, 0x94, 0x84, 0x86, 0xb8, 0x32, 0xf1, 0x56, 0x12, 0xba, 0xb6, 0x51, 0xc0, 0x21,
0x5b, 0x61, 0xfb, 0xe6, 0x42, 0x37, 0xb9, 0x5d, 0xb2, 0x87, 0xf2, 0x9f, 0x34, 0x97, 0x5c, 0xc3, 0x72, 0x5c, 0x49,
0x03, 0xae, 0x18, 0x0f, 0x96, 0xa6, 0x01, 0x09, 0xf0, 0x5d, 0x39, 0x8e, 0x8b, 0xab, 0x49, 0xf0, 0x87, 0x82, 0xf9,
0xd4, 0x98, 0xe9, 0x46, 0x48, 0xb5, 0x84, 0x93, 0x66, 0xb0, 0x06, 0x4d, 0x1a, 0x0f, 0x4a, 0xd4, 0x7c, 0x83, 0x86,
0x0a, 0x71, 0xfc, 0x89, 0xa8, 0x42, 0x13, 0x0c, 0xc1, 0xc8, 0xbd, 0x42, 0x32, 0x5c, 0xb6, 0x2a, 0x5a, 0xa4, 0x4c,
0x8d, 0x49, 0xa5, 0x6a, 0x96, 0xcb, 0xc0, 0xc0, 0xa2, 0xdd, 0xea, 0x4b, 0x4b, 0x5c, 0x89, 0xdc, 0x34, 0xd4, 0xc2,
0xa4, 0x50, 0xde, 0x84, 0x93, 0xa3, 0xdf, 0xa5, 0xac, 0x77, 0x13, 0x9f, 0x5c, 0xe1, 0x93, 0xfb, 0x86, 0x0f, 0x65,
0xf2, 0x76, 0x31, 0x28, 0x82, 0xaf, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, 0x74, 0x41, 0xa0, 0x48,
0x36, 0x49, 0x07, 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xe6, 0x8a, 0x0d, 0x52, 0xf3, 0x4a, 0x33, 0x2f, 0x75,
0x1b, 0xf6, 0x7b, 0x59, 0x4a, 0x3a, 0x71, 0x41, 0x99, 0xd8, 0xbb, 0x8e, 0x36, 0x5e, 0x1a, 0x66, 0xc2, 0xfa, 0x15,
0xc6, 0x4e, 0x8d, 0x42, 0xa9, 0x14, 0x81, 0x38, 0x36, 0xbe, 0x56, 0x96, 0x41, 0xe6, 0xaf, 0xb1, 0xa7, 0x00, 0x94,
0x04, 0x16, 0x5f, 0x53, 0xc9, 0x8b, 0xc2, 0x3a, 0x1d, 0xef, 0x10, 0x1d, 0x2b, 0x11, 0x5a, 0x13, 0xf9, 0x5a, 0x9f,
0xc5, 0x7e, 0xcd, 0x25, 0x34, 0x29, 0x99, 0x0f, 0xf2, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, 0x25, 0x83, 0x84, 0x1c,
0xd2, 0x55, 0xa2, 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x0a, 0x89, 0x96, 0x0e, 0xc3, 0xc8, 0x41, 0xc7, 0x9d, 0xd6, 0x62,
0x85, 0x90, 0x4d, 0x7b, 0x93, 0x58, 0x11, 0x9d, 0xd3, 0x1c, 0x4d, 0x38, 0x53, 0xa7, 0x3b, 0x0e, 0xa0, 0x03, 0x62,
0x7f, 0x85, 0xf5, 0xd6, 0x9a, 0x9d, 0xae, 0x5f, 0x39, 0x7c, 0x97, 0x97, 0x11, 0xf2, 0x83, 0x30, 0x78, 0x61, 0xcd,
0x06, 0x4a, 0xf6, 0xee, 0xbd, 0xc4, 0x56, 0x64, 0x7f, 0x56, 0x25, 0x95, 0xa7, 0x50, 0xe3, 0xdc, 0xfa, 0x3a, 0x31,
0x33, 0xb4, 0xa8, 0x2a, 0xf6, 0x0d, 0xa9, 0xbe, 0xaf, 0x14, 0x76, 0x85, 0xf2, 0xbe, 0x1c, 0x3a, 0x76, 0x5d, 0x37,
0xc8, 0xc9, 0x79, 0xb9, 0xb3, 0xce, 0x85, 0xbc, 0x7b, 0xd7, 0xf4, 0x99, 0x4e, 0xf5, 0xf0, 0x4f, 0x1c, 0x54, 0xce,
0xc5, 0x45, 0x4a, 0x16, 0xcc, 0x13, 0xa5, 0x8e, 0x56, 0x1c, 0xd0, 0x76, 0x0f, 0x3d, 0xed, 0xe8, 0x2c, 0x8a, 0xb9,
0xa5, 0x47, 0x11, 0x9e, 0x36, 0xca, 0x27, 0x69, 0x74, 0x00, 0x5e, 0x68, 0x42, 0x92, 0x13, 0x6e, 0xda, 0xa2, 0xc5,
0x68, 0xca, 0x30, 0x04, 0xae, 0xec, 0x09, 0x53, 0xf6, 0xdc, 0x41, 0xbc, 0xc5, 0xc0, 0x6c, 0x3d, 0xec, 0x65, 0xb3,
0x7b, 0xcd, 0xfc, 0x87, 0x35, 0x02, 0xd9, 0x36, 0x53, 0x75, 0x65, 0xe3, 0x5d, 0x8a, 0x48, 0x8c, 0xb0, 0xad, 0x1b,
0x5b, 0xda, 0xfa, 0xbd, 0x86, 0x7b, 0x5d, 0x39, 0xe6, 0x35, 0xa5, 0xda, 0xd0, 0xc3, 0xca, 0xcd, 0x61, 0xa6, 0x23,
0x2f, 0x56, 0xd0, 0xed, 0x89, 0xa0, 0x10, 0x38, 0x11, 0xda, 0x1e, 0x54, 0xdc, 0x40, 0xa4, 0xe4, 0x4a, 0xab, 0x66,
0x8b, 0x64, 0x2c, 0x81, 0x05, 0x17, 0x96, 0x4b, 0x3e, 0x3a, 0x8b, 0x93, 0xa4, 0x2a, 0xfd, 0x43, 0x05, 0xbc, 0x18,
0xf6, 0x26, 0xd1, 0x2e, 0x30, 0x5a, 0x28, 0x10, 0x5c, 0x6d, 0x84, 0xbd, 0x77, 0xdc, 0x6a, 0xdd, 0x45, 0xc4, 0x91,
0x9b, 0xd1, 0x08, 0xa8, 0xc7, 0x08, 0xab, 0x66, 0xed, 0xbd, 0x67, 0x18, 0x52, 0x33, 0xf0, 0x41, 0x75, 0x46, 0xc5,
0x9f, 0x65, 0x4f, 0x7d, 0x26, 0x7a, 0x37, 0xaa, 0xae, 0x66, 0x40, 0x45, 0x05, 0x3e, 0xcc, 0x10, 0x4b, 0x5b, 0x05,
0x02, 0x72, 0x3d, 0x2c, 0x4a, 0x01, 0x93, 0x34, 0x58, 0x50, 0x0a, 0xac, 0xb5, 0xb2, 0x7b, 0x79, 0x53, 0x30, 0x87,
0x42, 0xe1, 0xa2, 0xff, 0x93, 0x6c, 0x36, 0x47, 0xcb, 0xac, 0xc1, 0xd4, 0xd0, 0xe0, 0x7d, 0xa3, 0xbe, 0x5c, 0x53,
0x56, 0xeb, 0x43, 0x3b, 0xb2, 0xc6, 0x4f, 0xda, 0x51, 0x06, 0x87, 0x6a, 0xa1, 0x8b, 0xea, 0x76, 0x73, 0x53, 0xc4,
0xac, 0xe3, 0x71, 0x9f, 0xf4, 0xb6, 0xb6, 0x26, 0x3d, 0x4d, 0x03, 0x92, 0x49, 0x92, 0xe1, 0x4d, 0x06, 0x28, 0x2b,
0xe2, 0x2c, 0xcb, 0x06, 0xf9, 0x96, 0x65, 0x89, 0xeb, 0xf7, 0x6d, 0x6f, 0xaf, 0xe6, 0x59, 0x7b, 0x7b, 0x57, 0xbb,
0xc8, 0x55, 0x9d, 0xf4, 0x20, 0x0f, 0x87, 0x50, 0xb4, 0x62, 0x53, 0x86, 0xcb, 0x59, 0x36, 0x66, 0x81, 0x0d, 0xdd,
0x53, 0xbb, 0x94, 0x9b, 0x26, 0x81, 0xcd, 0x91, 0x30, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4,
0xb9, 0xf0, 0x5d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62,
0x35, 0x94, 0xd4, 0x77, 0x83, 0xef, 0x82, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11,
0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x9b, 0xb2, 0xf0, 0xe5, 0xe0, 0xa5,
0x72, 0xa9, 0x33, 0x70, 0x21, 0xc0, 0xc9, 0x4f, 0x62, 0xde, 0xc2, 0xf3, 0x48, 0xdb, 0x5b, 0x8a, 0x0a, 0x8c, 0x2b,
0x52, 0x5c, 0xba, 0x54, 0xde, 0xa0, 0xf7, 0x31, 0x3c, 0x82, 0x66, 0x1b, 0x1b, 0x4b, 0xe7, 0x55, 0xc4, 0xa7, 0x7e,
0x1e, 0xa5, 0xe3, 0x6c, 0xe6, 0xb8, 0x9b, 0xb6, 0xed, 0xfa, 0x05, 0x79, 0x22, 0x5f, 0xba, 0xe5, 0xc6, 0x91, 0x37,
0x62, 0xa1, 0x3d, 0xb0, 0x37, 0x3f, 0x7a, 0x07, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x8e, 0x58, 0xd9, 0x3f, 0xf2, 0xce,
0x75, 0xcc, 0xdd, 0x7b, 0x8b, 0x52, 0x06, 0x7a, 0x85, 0xfd, 0x73, 0x09, 0x06, 0xb0, 0x1b, 0xc5, 0xdf, 0x41, 0xca,
0xbd, 0xa7, 0x03, 0x11, 0x19, 0xa7, 0xbd, 0xbc, 0xb4, 0x33, 0x8a, 0x18, 0xd8, 0x77, 0xb4, 0xb3, 0x7a, 0xf7, 0x6e,
0xa5, 0xe6, 0xab, 0x52, 0x6f, 0xc4, 0xc2, 0x9a, 0xa7, 0xee, 0x1d, 0xd0, 0xd1, 0x4a, 0x7d, 0x23, 0x8f, 0x18, 0x29,
0xcd, 0x55, 0x3b, 0xc1, 0x31, 0xb6, 0xf8, 0xfa, 0x6d, 0x7d, 0x28, 0xa2, 0x14, 0x7e, 0x0c, 0xd6, 0x4b, 0x04, 0xea,
0x1b, 0x1c, 0x1c, 0xef, 0x20, 0xdc, 0xda, 0x75, 0x06, 0x81, 0x73, 0xa7, 0xd5, 0xba, 0xfc, 0x69, 0xeb, 0xf0, 0xe7,
0xa8, 0xf5, 0xdb, 0x5e, 0xeb, 0xc7, 0xa1, 0x7b, 0xe9, 0xfc, 0xb4, 0x35, 0x38, 0x94, 0x6f, 0x87, 0x3f, 0xf7, 0x7f,
0x2a, 0x86, 0x5f, 0x8a, 0xc2, 0x0d, 0xd7, 0xdd, 0x3a, 0x01, 0x4f, 0x29, 0xdc, 0x6a, 0xb5, 0xfa, 0xf0, 0xb4, 0x80,
0x27, 0xfc, 0x79, 0x06, 0x3f, 0x2e, 0x0f, 0xad, 0xff, 0xf0, 0x53, 0xfa, 0x1f, 0x7f, 0xca, 0x87, 0x38, 0xe6, 0xe1,
0xcf, 0x3f, 0x15, 0xf6, 0xbd, 0x7e, 0xb8, 0x35, 0xdc, 0x74, 0x1d, 0x5d, 0xf3, 0x65, 0x58, 0x3d, 0x42, 0xab, 0xc3,
0x9f, 0xe5, 0x9b, 0x7d, 0xef, 0x68, 0xb7, 0x1f, 0x0e, 0x2f, 0x1d, 0xfb, 0xf2, 0x9e, 0x7b, 0xe9, 0xba, 0x97, 0x1b,
0x38, 0xcf, 0x1c, 0x46, 0xbf, 0x07, 0x3f, 0x4f, 0xe0, 0xa7, 0x0d, 0x3f, 0x4f, 0xe1, 0xe7, 0xcf, 0xd0, 0x4d, 0xc4,
0xdf, 0x2e, 0x29, 0x16, 0x72, 0x89, 0x07, 0x16, 0x11, 0xac, 0x82, 0xbb, 0xb1, 0x15, 0x7b, 0x13, 0x22, 0x1a, 0xec,
0x43, 0xdf, 0xf7, 0x31, 0x4c, 0xea, 0x2c, 0x3f, 0x6e, 0xc0, 0xa2, 0x23, 0xe7, 0x6c, 0x04, 0xcc, 0x13, 0x91, 0x83,
0x22, 0xe0, 0xe2, 0x6c, 0xb5, 0xc0, 0xc3, 0x55, 0x6f, 0x1c, 0x4e, 0x98, 0x03, 0x46, 0xc1, 0x73, 0x86, 0x0f, 0x5d,
0xd7, 0x7b, 0x26, 0xcf, 0x0c, 0x71, 0x9f, 0x0b, 0xd6, 0x4a, 0x33, 0x61, 0xd2, 0xd8, 0xae, 0x37, 0x5f, 0x53, 0x09,
0xdb, 0x3a, 0x3d, 0x81, 0xba, 0x0d, 0x71, 0xd0, 0xf6, 0x3d, 0x8b, 0x3e, 0xe1, 0x96, 0x7c, 0x6d, 0x1c, 0x02, 0xaf,
0x58, 0xf2, 0x4d, 0xa3, 0xd1, 0xb0, 0x11, 0x85, 0x3b, 0xf6, 0x98, 0xc1, 0x0c, 0x2b, 0x26, 0x22, 0x27, 0xa5, 0x29,
0x2c, 0x5b, 0x98, 0xfc, 0x6d, 0x94, 0xf3, 0x8d, 0xca, 0xb0, 0x0d, 0x6b, 0x96, 0x6c, 0xd3, 0xd2, 0xbf, 0xc5, 0x14,
0x68, 0x5a, 0xd2, 0xf9, 0x87, 0x39, 0x7e, 0x98, 0x12, 0x5a, 0xaf, 0x1d, 0x0e, 0x1e, 0x7a, 0x01, 0x72, 0x47, 0xf4,
0x73, 0xde, 0xa2, 0x1a, 0x83, 0xbf, 0x32, 0xcc, 0xe0, 0x89, 0xf9, 0x30, 0x44, 0xb3, 0x2c, 0x75, 0x70, 0x2b, 0x45,
0x71, 0xff, 0x02, 0x77, 0x46, 0x5a, 0x7a, 0xfb, 0xa1, 0xda, 0x31, 0x07, 0x39, 0x63, 0xdf, 0x47, 0xc9, 0x27, 0x96,
0x3b, 0xe7, 0x5e, 0xa7, 0xfb, 0x15, 0x75, 0xf6, 0xd0, 0x36, 0x7b, 0x55, 0x1d, 0xa3, 0x29, 0xb3, 0x40, 0x1d, 0x11,
0xb6, 0x3a, 0x5e, 0x8e, 0x51, 0x2d, 0x24, 0x41, 0xe1, 0x65, 0x61, 0x97, 0x38, 0xdc, 0xde, 0x2d, 0x4e, 0x4f, 0xfa,
0x76, 0x60, 0xdb, 0x60, 0xf1, 0x1f, 0x50, 0xd8, 0x4a, 0x18, 0x16, 0x60, 0x90, 0xed, 0xc6, 0x3d, 0xbe, 0xb9, 0x59,
0x05, 0x9c, 0xf0, 0x20, 0x9d, 0xba, 0x27, 0x5e, 0xe4, 0x4d, 0x43, 0x18, 0x70, 0x04, 0xcd, 0xb0, 0x4b, 0x6f, 0xb4,
0x1b, 0xcb, 0x69, 0x30, 0x16, 0xe2, 0x27, 0x51, 0xc1, 0x5f, 0x60, 0x3c, 0x22, 0x1c, 0xa1, 0xb1, 0xef, 0xb3, 0x73,
0x36, 0x52, 0x76, 0x06, 0x10, 0x2a, 0x72, 0x7b, 0xee, 0x28, 0x34, 0x9a, 0xc1, 0xdc, 0x61, 0x78, 0x30, 0xb0, 0x61,
0x2f, 0xc1, 0xae, 0x0c, 0xa3, 0xc3, 0xce, 0x70, 0x90, 0x86, 0x20, 0x6b, 0x35, 0x6d, 0x65, 0xd1, 0xa2, 0x56, 0xd4,
0x1d, 0x0e, 0x9c, 0x53, 0x30, 0xd2, 0xc1, 0x16, 0x77, 0xf0, 0x0d, 0x23, 0x14, 0x45, 0xf8, 0x8e, 0x9d, 0x3c, 0x3b,
0x9f, 0x3b, 0xf6, 0xee, 0x96, 0xbd, 0x89, 0xa5, 0x9e, 0x0d, 0xec, 0x05, 0x73, 0x87, 0x67, 0xae, 0xd9, 0x79, 0x7b,
0x88, 0xa0, 0x62, 0x21, 0x4e, 0x7e, 0x36, 0xb0, 0xfb, 0x62, 0xea, 0x36, 0x0c, 0x9a, 0xca, 0xe5, 0xc7, 0x15, 0x3d,
0x20, 0x54, 0x55, 0x57, 0x05, 0x1d, 0x94, 0x75, 0x03, 0x67, 0x6a, 0x22, 0xd1, 0xc2, 0xc9, 0x24, 0x15, 0xc0, 0xe1,
0xc1, 0x66, 0x30, 0xa9, 0xd1, 0x6d, 0x7b, 0x38, 0x38, 0x0b, 0xee, 0xd9, 0xf7, 0xd4, 0xcb, 0x09, 0x0b, 0xc0, 0xbb,
0xa0, 0xe9, 0x4f, 0x50, 0x8b, 0xc0, 0xcf, 0x39, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x2c, 0x8b, 0x16, 0x58, 0x74, 0x10,
0xa5, 0xe6, 0xab, 0x52, 0xf0, 0x3e, 0xc2, 0x9a, 0xa7, 0xee, 0x3d, 0xa7, 0xa3, 0x95, 0xfa, 0x46, 0x1e, 0x33, 0x52,
0x9a, 0xab, 0x76, 0x82, 0x63, 0x6c, 0xf1, 0xf5, 0xdb, 0xfa, 0x50, 0x44, 0x29, 0xfc, 0x18, 0xac, 0x97, 0x08, 0xd4,
0x37, 0x38, 0x38, 0xde, 0x41, 0xb8, 0xb5, 0xeb, 0x0c, 0x02, 0xe7, 0x4e, 0xab, 0x75, 0xf9, 0xd3, 0xd6, 0xe1, 0xcf,
0x51, 0xeb, 0xb7, 0xbd, 0xd6, 0x8f, 0x43, 0xf7, 0xd2, 0xf9, 0x69, 0x6b, 0x70, 0x28, 0xdf, 0x0e, 0x7f, 0xee, 0xff,
0x54, 0x0c, 0xff, 0x24, 0x0a, 0x37, 0x5c, 0x77, 0xeb, 0xc4, 0x5b, 0xb0, 0x70, 0xab, 0xd5, 0xea, 0xc3, 0xd3, 0x1c,
0x9e, 0xf0, 0xe7, 0x19, 0xfc, 0xb8, 0x3c, 0xb4, 0xfe, 0xd3, 0x4f, 0xe9, 0xdf, 0xfc, 0x94, 0x0f, 0x71, 0xcc, 0xc3,
0x9f, 0x7f, 0x2a, 0xec, 0x7b, 0xfd, 0x70, 0x6b, 0xb8, 0xe9, 0x3a, 0xba, 0xe6, 0x4f, 0x61, 0xf5, 0x08, 0xad, 0x0e,
0x7f, 0x96, 0x6f, 0xf6, 0xbd, 0xa3, 0xdd, 0x7e, 0x38, 0xbc, 0x74, 0xec, 0xcb, 0x7b, 0xee, 0xa5, 0xeb, 0x5e, 0x6e,
0xe0, 0x3c, 0x27, 0x30, 0xfa, 0x3d, 0xf8, 0x79, 0x0a, 0x3f, 0x6d, 0xf8, 0x39, 0x81, 0x9f, 0x3f, 0x43, 0x37, 0x11,
0x7f, 0xbb, 0xa4, 0x58, 0xc8, 0x25, 0x1e, 0x58, 0x44, 0xb0, 0x0a, 0xee, 0xc6, 0x56, 0xec, 0x6d, 0x10, 0xd1, 0x60,
0x1f, 0xfa, 0xbe, 0x8f, 0x61, 0x52, 0x67, 0xf9, 0x71, 0x03, 0x16, 0x1d, 0x39, 0x67, 0x23, 0x60, 0x9e, 0x88, 0x1c,
0x14, 0x01, 0x17, 0x67, 0xab, 0x05, 0x1e, 0xae, 0x7a, 0xe3, 0x70, 0x83, 0x39, 0x60, 0x14, 0xbc, 0x66, 0xf8, 0xd0,
0x75, 0xbd, 0x67, 0xf2, 0xcc, 0x10, 0xf7, 0xb9, 0x60, 0xad, 0x34, 0x13, 0x26, 0x8d, 0xed, 0x7a, 0xf3, 0x35, 0x95,
0xb0, 0xad, 0xd3, 0x13, 0xa8, 0x9b, 0x89, 0x83, 0xb6, 0xef, 0x58, 0xf4, 0x09, 0xb7, 0xe4, 0x2b, 0xe3, 0x10, 0x78,
0xc5, 0x92, 0x6f, 0x1a, 0x8d, 0x86, 0x8d, 0x28, 0xdc, 0xb1, 0xc7, 0x0c, 0x66, 0x58, 0x31, 0x11, 0x39, 0x29, 0x4d,
0x61, 0xd9, 0xc2, 0xe4, 0x6f, 0xa3, 0x9c, 0x6f, 0x54, 0x86, 0x6d, 0x58, 0xb3, 0x64, 0x9b, 0x96, 0xfe, 0x2d, 0xa6,
0x40, 0xd3, 0x92, 0xce, 0x3f, 0xcc, 0xf1, 0xc3, 0x94, 0xd0, 0x7a, 0xed, 0x70, 0xf0, 0xd0, 0x0b, 0x90, 0x3b, 0xa2,
0x9f, 0xf3, 0x16, 0xd5, 0x18, 0xfc, 0x95, 0x61, 0x06, 0x4f, 0xcc, 0x87, 0x21, 0x9a, 0x65, 0xa9, 0x83, 0x5b, 0x29,
0x8a, 0xfb, 0x17, 0xb8, 0x33, 0xd2, 0xd2, 0xdb, 0x0f, 0xd5, 0x8e, 0x39, 0xc8, 0x19, 0xfb, 0x2e, 0x4a, 0x3e, 0xb1,
0xdc, 0x39, 0xf7, 0x3a, 0xdd, 0x2f, 0xa9, 0xb3, 0x87, 0xb6, 0xd9, 0xbb, 0xea, 0x18, 0x4d, 0x99, 0x05, 0xea, 0x88,
0xb0, 0xd5, 0xf1, 0x72, 0x8c, 0x6a, 0x21, 0x09, 0x0a, 0x2f, 0x0b, 0xbb, 0xc4, 0xe1, 0xf6, 0x6e, 0x71, 0x7a, 0xd2,
0xb7, 0x03, 0xdb, 0x06, 0x8b, 0xff, 0x80, 0xc2, 0x56, 0xc2, 0xb0, 0x00, 0x83, 0x6c, 0x37, 0xee, 0xf1, 0xcd, 0xcd,
0x2a, 0xe0, 0x84, 0x07, 0xe9, 0xd4, 0x3d, 0xf1, 0x22, 0x6f, 0x1a, 0xc2, 0x80, 0x23, 0x68, 0x86, 0x5d, 0x7a, 0xa3,
0xdd, 0x58, 0x4e, 0x83, 0xb1, 0x10, 0x3f, 0x89, 0x0a, 0xfe, 0x02, 0xe3, 0x11, 0xe1, 0x08, 0x8d, 0x7d, 0x9f, 0x9d,
0xb3, 0x91, 0xb2, 0x33, 0x80, 0x50, 0x91, 0xdb, 0x73, 0x47, 0xa1, 0xd1, 0x0c, 0xe6, 0x0e, 0xc3, 0x83, 0x81, 0x0d,
0x7b, 0x09, 0x76, 0x65, 0x18, 0x1d, 0x76, 0x86, 0x83, 0x34, 0x5c, 0xb0, 0x40, 0xd3, 0x56, 0x16, 0xcd, 0x6b, 0x45,
0xdd, 0xe1, 0xc0, 0x99, 0x80, 0x91, 0x0e, 0xb6, 0xb8, 0x83, 0x6f, 0x18, 0xa1, 0x28, 0xc2, 0x77, 0xec, 0xe4, 0xd9,
0xf9, 0xdc, 0xb1, 0x77, 0xb7, 0xec, 0x4d, 0x2c, 0xf5, 0x6c, 0x60, 0x2f, 0x98, 0x3b, 0x3c, 0x73, 0xcd, 0xce, 0xdb,
0x43, 0x04, 0x15, 0x0b, 0x71, 0xf2, 0xb3, 0x81, 0xdd, 0x17, 0x53, 0xb7, 0x61, 0xd0, 0x54, 0x2e, 0x3f, 0xae, 0xe8,
0x01, 0xa1, 0xaa, 0xba, 0x2a, 0xe8, 0xa0, 0xac, 0x1b, 0x38, 0x53, 0x13, 0x89, 0x16, 0x4e, 0x26, 0xa9, 0x00, 0x0e,
0x0f, 0x36, 0x83, 0x49, 0x8d, 0x6e, 0xdb, 0xc3, 0xc1, 0x59, 0x70, 0xcf, 0xbe, 0xa7, 0x5e, 0x4e, 0x59, 0x70, 0xc2,
0xc4, 0xf4, 0xa7, 0x20, 0xed, 0xf0, 0xe7, 0x09, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x21, 0x8b, 0xe6, 0x58, 0x74, 0x10,
0x20, 0xa8, 0x5e, 0xa1, 0xad, 0x3f, 0xb1, 0x26, 0xe3, 0x90, 0x60, 0xbf, 0x7b, 0x17, 0x96, 0x66, 0xb3, 0x33, 0xc4,
0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc7, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36,
0x43, 0x41, 0xe1, 0x78, 0xf3, 0x3d, 0x0b, 0xa6, 0xfd, 0xb0, 0x3d, 0x70, 0x72, 0xa1, 0x3a, 0x12, 0x3c, 0xb7, 0x14,
0x12, 0xbc, 0xed, 0x4d, 0x41, 0xa0, 0x23, 0xe7, 0x6e, 0xd8, 0x9b, 0xaa, 0x10, 0x8a, 0x3e, 0x6e, 0x8e, 0xdd, 0x20,
0x86, 0x1f, 0x4e, 0x0b, 0x99, 0x66, 0xaa, 0xfb, 0x6a, 0xcd, 0xec, 0x06, 0x63, 0x65, 0x91, 0x27, 0x61, 0xb6, 0xe9,
0x60, 0x84, 0x16, 0x24, 0xed, 0xee, 0x00, 0x60, 0xd8, 0x74, 0x14, 0xa7, 0x6d, 0x29, 0x56, 0x53, 0xf6, 0xf9, 0x61,
0xb5, 0x1c, 0x6c, 0x10, 0x31, 0xbf, 0xd2, 0x3e, 0x00, 0x56, 0x90, 0x78, 0xf9, 0x50, 0x9d, 0x79, 0x3d, 0xaf, 0x9d,
0x6f, 0x2d, 0x95, 0x28, 0x62, 0x9e, 0x21, 0xa1, 0x78, 0xa9, 0xdd, 0x30, 0x61, 0x6e, 0xcf, 0x91, 0x18, 0x9a, 0xe5,
0xc3, 0x36, 0x30, 0xbd, 0x0a, 0xb0, 0xa7, 0xe6, 0xb6, 0x48, 0xc2, 0xaa, 0xb9, 0x77, 0x08, 0xac, 0x3d, 0x0c, 0x5f,
0x89, 0x13, 0xc7, 0x9e, 0x8a, 0xe6, 0xb3, 0x24, 0x7c, 0xde, 0x38, 0x2e, 0x8e, 0xf0, 0x44, 0x68, 0xdf, 0x1f, 0x2d,
0x72, 0x90, 0x07, 0xfc, 0x35, 0x58, 0x06, 0xa1, 0x6c, 0x8a, 0x8e, 0x1e, 0x1e, 0x01, 0x7b, 0x84, 0x78, 0x23, 0x6c,
0x6e, 0x54, 0xa3, 0x45, 0x49, 0xc6, 0x0b, 0x1d, 0x0c, 0xf7, 0xb8, 0x74, 0xed, 0x51, 0x30, 0xc8, 0x13, 0x63, 0x07,
0xcf, 0xfc, 0xfd, 0x11, 0x56, 0xe3, 0x04, 0x85, 0x5b, 0xd2, 0x6e, 0xab, 0xc4, 0xdf, 0xbe, 0x9f, 0x82, 0x04, 0xc7,
0x3a, 0xf0, 0xb3, 0xee, 0xde, 0x4d, 0x24, 0x52, 0xbb, 0x69, 0x8f, 0x4e, 0x22, 0x30, 0x1e, 0x9c, 0xfb, 0x29, 0x54,
0x23, 0x89, 0xa8, 0x28, 0x47, 0x0b, 0xd4, 0x3c, 0x55, 0xab, 0xe0, 0x3b, 0x34, 0x23, 0xf0, 0x1c, 0xc3, 0xd6, 0xe4,
0xa7, 0xea, 0xc6, 0x22, 0x96, 0xef, 0xba, 0x74, 0xb4, 0x85, 0x07, 0x90, 0x82, 0xd1, 0x04, 0xc3, 0xb8, 0x14, 0x94,
0xac, 0xf8, 0xef, 0xa3, 0x11, 0x2b, 0x9f, 0x1e, 0x66, 0x9b, 0x9b, 0x43, 0x71, 0x6e, 0x41, 0x8c, 0xc3, 0x8d, 0xe8,
0x6a, 0x5c, 0x01, 0x50, 0x9f, 0xce, 0x89, 0xeb, 0x81, 0x69, 0xc5, 0x9a, 0x2e, 0xc5, 0x3e, 0x39, 0xcc, 0x00, 0x14,
0xdc, 0x72, 0x0e, 0xfd, 0xc1, 0x9f, 0x86, 0xe0, 0x1e, 0xfb, 0x5f, 0xba, 0x5b, 0x4a, 0xd0, 0xf4, 0xe4, 0x99, 0xe2,
0x92, 0xce, 0x58, 0x3b, 0x1e, 0xc5, 0x46, 0x83, 0xc2, 0x4b, 0x01, 0x03, 0xd0, 0xe6, 0x20, 0x13, 0x2a, 0x0e, 0x42,
0x8e, 0x0a, 0x6c, 0x1f, 0x37, 0x3f, 0xc7, 0x9d, 0xfd, 0x14, 0x2c, 0xbc, 0x81, 0x7e, 0x7b, 0x0c, 0x6f, 0x7f, 0xd2,
0x6f, 0x2f, 0x59, 0xf0, 0x4b, 0x29, 0x43, 0xf7, 0xb5, 0x29, 0x1e, 0xa8, 0x29, 0x4a, 0xb1, 0x44, 0x06, 0x0d, 0x99,
0x9b, 0xaf, 0xc4, 0x6c, 0xb8, 0x5b, 0xa2, 0xda, 0x91, 0xa2, 0x2b, 0xf7, 0x79, 0x74, 0x82, 0xc4, 0x75, 0x4d, 0x52,
0x18, 0xb9, 0x04, 0x26, 0xc2, 0x15, 0xdf, 0x12, 0x73, 0xf6, 0xdb, 0x60, 0x83, 0xd7, 0xf2, 0x0e, 0xd0, 0xbe, 0x63,
0xb3, 0x39, 0xbf, 0xd8, 0x27, 0x45, 0x1f, 0xc8, 0xb4, 0x01, 0x71, 0x76, 0xde, 0xee, 0xc5, 0xbb, 0xbc, 0x17, 0x83,
0x54, 0xcf, 0x15, 0x8b, 0xe1, 0x5e, 0xf5, 0xde, 0x62, 0x94, 0xd2, 0x64, 0x26, 0xaf, 0x86, 0x5e, 0x57, 0xa2, 0xb7,
0xb9, 0x09, 0x08, 0xf6, 0x8c, 0xae, 0x5c, 0x74, 0x2d, 0x4b, 0x41, 0x13, 0x80, 0xe8, 0x51, 0x9d, 0xe5, 0x88, 0xe3,
0x30, 0x9b, 0x0d, 0x05, 0x07, 0x73, 0xd7, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, 0xb0, 0x03, 0x98, 0x11, 0x97, 0xb7,
0x3a, 0x22, 0x3a, 0x2c, 0xfa, 0xeb, 0xf8, 0xf6, 0x47, 0x8f, 0x6d, 0x76, 0x5c, 0xd0, 0x20, 0xb5, 0xb1, 0x1e, 0x56,
0x63, 0x41, 0x7d, 0xf8, 0x51, 0x53, 0xa9, 0x2c, 0x36, 0x37, 0xcb, 0xfa, 0x51, 0xad, 0xda, 0xc1, 0xb5, 0xd3, 0x94,
0xf3, 0x66, 0x36, 0x08, 0x07, 0x22, 0x26, 0x50, 0xa0, 0xa5, 0x95, 0x15, 0x03, 0x0c, 0x29, 0xcb, 0x51, 0x3e, 0x85,
0xcc, 0x8b, 0xcb, 0x52, 0xa7, 0xbe, 0xc8, 0x78, 0x64, 0x88, 0xa7, 0x9e, 0x64, 0xac, 0x80, 0x82, 0xf5, 0x52, 0x2f,
0xa1, 0x25, 0x02, 0xcc, 0x9f, 0xa9, 0x1c, 0x1a, 0x61, 0x81, 0x44, 0xa1, 0x61, 0x96, 0x28, 0xe3, 0xb3, 0x08, 0x63,
0xd0, 0xf6, 0x4f, 0x6a, 0xb1, 0xaf, 0x42, 0x19, 0x1d, 0xc5, 0x61, 0x3e, 0x0c, 0xa8, 0x7e, 0x21, 0x25, 0xd8, 0x34,
0x7c, 0x0f, 0x6c, 0x54, 0x39, 0x9e, 0x24, 0x08, 0x9f, 0xc6, 0x39, 0x23, 0x4f, 0x61, 0x43, 0xc2, 0x2c, 0x4d, 0xdb,
0x48, 0xb5, 0x8b, 0xcc, 0x20, 0x94, 0x0b, 0xf3, 0x4f, 0x8d, 0xb3, 0x8b, 0x2c, 0x5c, 0x69, 0x0d, 0xe6, 0xc7, 0x1b,
0x13, 0xa0, 0xec, 0xf2, 0x32, 0x13, 0x3e, 0x6e, 0x44, 0xf6, 0x86, 0xae, 0x98, 0x0e, 0x14, 0x52, 0x81, 0x13, 0x91,
0xc5, 0x43, 0x67, 0x28, 0x34, 0xc2, 0x01, 0x9d, 0x22, 0xe7, 0xae, 0xb1, 0xe9, 0xf3, 0x81, 0xf6, 0x8d, 0xd2, 0xd0,
0x49, 0x40, 0x08, 0x08, 0xdc, 0x0d, 0x6b, 0x2a, 0x1d, 0xa4, 0x41, 0x42, 0xa5, 0xe8, 0xe7, 0x00, 0xfe, 0x61, 0x24,
0x29, 0x00, 0xf6, 0x43, 0x35, 0x52, 0x44, 0x59, 0x16, 0xb8, 0x00, 0x34, 0xd7, 0x3e, 0xae, 0x84, 0x2f, 0x0c, 0x54,
0x98, 0x9e, 0x66, 0xe5, 0xa5, 0x50, 0x22, 0xef, 0xd6, 0xa4, 0xac, 0x91, 0x4c, 0x3e, 0x45, 0x87, 0x4f, 0x79, 0xd7,
0xaf, 0x25, 0x1e, 0xba, 0xe0, 0x29, 0x2c, 0xab, 0x7a, 0x7e, 0x15, 0x72, 0x72, 0xae, 0x41, 0x57, 0x48, 0xa1, 0xbf,
0xe2, 0x24, 0xef, 0xbd, 0xf2, 0xab, 0x5a, 0x6a, 0x0c, 0x65, 0xef, 0xd7, 0x35, 0xc3, 0xf2, 0x72, 0x5e, 0x85, 0x29,
0x08, 0xb8, 0x25, 0x4b, 0x82, 0xa5, 0xd4, 0x10, 0x60, 0x61, 0x7b, 0xa4, 0x95, 0x82, 0xbc, 0xd4, 0xe1, 0x9d, 0xa7,
0x60, 0x05, 0x18, 0x87, 0x5a, 0x2a, 0x99, 0x46, 0x12, 0x5f, 0x2a, 0x51, 0x60, 0xca, 0xfd, 0x11, 0xf8, 0xa9, 0xcd,
0x93, 0xae, 0x73, 0xd7, 0x8f, 0x67, 0x98, 0xda, 0x43, 0xa0, 0xc7, 0xde, 0x1d, 0x30, 0x25, 0xea, 0x3a, 0xac, 0x20,
0x0e, 0xcd, 0x6a, 0x9a, 0x05, 0xcc, 0x98, 0x36, 0x68, 0xc9, 0x36, 0xd8, 0x72, 0x39, 0xd8, 0x47, 0x62, 0x7b, 0x56,
0x2b, 0x20, 0x74, 0x0d, 0x1a, 0x18, 0x72, 0x97, 0x0a, 0x2d, 0xcc, 0x7b, 0x5d, 0x2a, 0xc2, 0xfd, 0x39, 0xe0, 0xd2,
0x0a, 0xce, 0xbc, 0x8c, 0x06, 0xde, 0x8f, 0x8f, 0x13, 0x4c, 0x7c, 0x41, 0xac, 0xc0, 0x0e, 0x0e, 0x3a, 0xcd, 0xa6,
0xc0, 0xa9, 0xb8, 0x48, 0x19, 0x2c, 0x2b, 0x4a, 0x6d, 0xf8, 0x21, 0x45, 0xb6, 0xee, 0xf2, 0x40, 0x77, 0x21, 0x16,
0xc0, 0x4e, 0xbf, 0x60, 0xe4, 0x5b, 0xd6, 0xcb, 0x80, 0xc1, 0xa9, 0xd6, 0x38, 0x08, 0xfc, 0xe6, 0x66, 0x32, 0x2c,
0x53, 0x62, 0xbb, 0x26, 0xab, 0x0b, 0xc8, 0x61, 0xa8, 0x26, 0xee, 0x20, 0x2c, 0x95, 0x3d, 0x5e, 0x94, 0x33, 0x5c,
0x2e, 0x65, 0x21, 0x37, 0xcf, 0xab, 0x69, 0x3e, 0xb7, 0xd2, 0x6c, 0x3a, 0xde, 0x8a, 0x2f, 0x0a, 0xfe, 0x81, 0x13,
0x4b, 0xab, 0x9e, 0x52, 0x2b, 0x3c, 0xca, 0xdc, 0x92, 0x75, 0x4a, 0x6a, 0x75, 0xdd, 0x40, 0x35, 0xc2, 0xd3, 0x34,
0x6c, 0x04, 0x42, 0x4c, 0x70, 0xf1, 0xeb, 0x26, 0x13, 0xd3, 0xde, 0x12, 0x52, 0x47, 0xd8, 0x3d, 0x94, 0x13, 0xdc,
0xd5, 0x3c, 0xfb, 0x3c, 0x9c, 0x5f, 0xcd, 0xdc, 0x7b, 0x06, 0x73, 0x3f, 0x0e, 0xb9, 0xc1, 0xe8, 0xb1, 0x4c, 0xf8,
0x91, 0xb1, 0x8f, 0x5c, 0x55, 0x3d, 0x39, 0x09, 0x2b, 0x91, 0x25, 0x9e, 0x8c, 0xa3, 0x0e, 0xe3, 0x54, 0xb4, 0x26,
0xc8, 0x2e, 0x2f, 0x0b, 0x73, 0x2f, 0x50, 0xd0, 0xd4, 0xe3, 0xf5, 0x38, 0x6d, 0xc5, 0xce, 0x46, 0x24, 0x72, 0xef,
0x55, 0x2d, 0x12, 0x59, 0xf1, 0x39, 0x8e, 0x74, 0xc5, 0x41, 0xee, 0x93, 0x93, 0xd5, 0x4d, 0x2a, 0x74, 0x8b, 0x46,
0xdb, 0xd8, 0xa3, 0xfa, 0x40, 0x52, 0xcf, 0xa8, 0xc0, 0xaa, 0xc6, 0xbe, 0x7b, 0xb7, 0x23, 0xd2, 0x2d, 0x95, 0x62,
0x83, 0xa5, 0x85, 0xd1, 0x8c, 0x51, 0x30, 0x28, 0x29, 0x32, 0x50, 0xa3, 0xfc, 0x0a, 0xc1, 0xb0, 0x47, 0x0d, 0x40,
0x71, 0xae, 0xaf, 0x7e, 0x5c, 0x4a, 0xb6, 0x10, 0x90, 0xb8, 0x4b, 0x06, 0x62, 0x4d, 0x30, 0x33, 0xf2, 0xc9, 0x7b,
0xe0, 0xbc, 0x01, 0x43, 0x1f, 0x01, 0xfc, 0x02, 0xb1, 0xe9, 0xc1, 0xc4, 0xb6, 0x89, 0x28, 0xfa, 0x6c, 0xe0, 0x39,
0x00, 0x3b, 0xaf, 0x42, 0xa3, 0xef, 0xaa, 0x14, 0x30, 0x64, 0x03, 0x37, 0x60, 0x55, 0x58, 0x6e, 0xef, 0x39, 0xb8,
0x0d, 0xf0, 0xfa, 0x4c, 0x36, 0xdf, 0xc0, 0x3c, 0xc1, 0xea, 0xec, 0xc2, 0xaf, 0x2c, 0x6b, 0x71, 0xee, 0x74, 0xd0,
0xa8, 0x57, 0x94, 0x10, 0xb5, 0xfb, 0x58, 0x7b, 0x80, 0x11, 0x16, 0xf1, 0xfe, 0x0a, 0xdf, 0xf5, 0xb8, 0xe5, 0x9e,
0x46, 0x8b, 0x30, 0x5d, 0x25, 0x8d, 0x41, 0xc9, 0xba, 0x9f, 0x8c, 0xb8, 0x97, 0xfb, 0x22, 0x16, 0x5c, 0xe1, 0xc8,
0xaa, 0x90, 0x62, 0x03, 0x49, 0x7a, 0xda, 0xa3, 0x03, 0xf6, 0x8d, 0x66, 0x2f, 0xa0, 0xcc, 0xfb, 0x8a, 0x54, 0x12,
0x52, 0x9a, 0xdd, 0x10, 0x49, 0xc2, 0x5a, 0x91, 0xa7, 0xce, 0xfb, 0x8e, 0xf6, 0xb9, 0x95, 0x44, 0x30, 0x82, 0x93,
0x30, 0x1d, 0x2b, 0x0f, 0x9a, 0x02, 0x5c, 0x45, 0x47, 0x4c, 0xdf, 0x04, 0xe4, 0x37, 0x03, 0xb9, 0xbd, 0x92, 0x5c,
0x9b, 0x6b, 0x18, 0x9e, 0x20, 0xc1, 0xaa, 0x48, 0x04, 0x1e, 0x51, 0x03, 0x8e, 0xf9, 0x3a, 0xcf, 0x03, 0x4c, 0xf8,
0xda, 0xde, 0x04, 0x80, 0x72, 0x72, 0x55, 0x9c, 0x95, 0x40, 0x37, 0x60, 0xb9, 0x3e, 0x4e, 0x8d, 0x8a, 0xc4, 0xc5,
0x8d, 0xe9, 0xea, 0x96, 0xfe, 0x0c, 0x2d, 0x67, 0x32, 0xc4, 0x74, 0x10, 0x04, 0x64, 0xea, 0x3b, 0xe6, 0x08, 0x99,
0x2b, 0xac, 0xcf, 0xb9, 0x53, 0x9b, 0xba, 0xc7, 0xa8, 0x9b, 0x27, 0xa9, 0xc5, 0xeb, 0xb4, 0x29, 0x25, 0x62, 0x52,
0x62, 0x6e, 0x88, 0x54, 0x6c, 0xa6, 0xc4, 0x9d, 0x5b, 0xdf, 0x68, 0x21, 0x6d, 0xb4, 0x0d, 0x91, 0x83, 0xcd, 0x2a,
0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x5f, 0x51, 0x92, 0x0e, 0x73, 0x4c, 0x04, 0xab, 0x17, 0x53, 0x91, 0xbf,
0x73, 0x74, 0x9a, 0xbd, 0x41, 0x0f, 0x52, 0x6f, 0x20, 0x31, 0x6b, 0xe2, 0xbb, 0x90, 0x86, 0x3a, 0x42, 0xa0, 0x32,
0xaa, 0x65, 0x3a, 0x4e, 0xac, 0xc2, 0x37, 0x82, 0xaf, 0xde, 0xea, 0xe3, 0x7c, 0xe3, 0xb9, 0xb1, 0x1a, 0x41, 0x0c,
0xde, 0x42, 0x3e, 0xf4, 0xa4, 0x08, 0x07, 0xc2, 0xe5, 0x9b, 0x9b, 0xbd, 0x7c, 0x97, 0x57, 0x21, 0x92, 0x0a, 0xc6,
0x18, 0x33, 0x8a, 0x71, 0x4f, 0xd4, 0xd4, 0x62, 0x0e, 0x03, 0xcb, 0xd6, 0x61, 0x8e, 0x07, 0x00, 0xd0, 0xd2, 0x94,
0x5e, 0x35, 0x15, 0x2a, 0xcf, 0x73, 0x09, 0x9f, 0xea, 0x10, 0x55, 0x35, 0x7e, 0xbb, 0x3e, 0x03, 0x85, 0xe0, 0xbe,
0xd3, 0xf1, 0xf0, 0x10, 0x02, 0x56, 0x51, 0xc8, 0x02, 0xbd, 0x41, 0x7b, 0x55, 0x22, 0x14, 0x33, 0x27, 0xeb, 0x31,
0xc3, 0x49, 0x05, 0x5b, 0xa8, 0x84, 0xa5, 0xd2, 0x02, 0xbf, 0xda, 0x08, 0xcd, 0x53, 0xc6, 0xbd, 0x57, 0x15, 0xce,
0xa0, 0x3f, 0x98, 0xb7, 0xca, 0xa8, 0x6f, 0x57, 0x4e, 0x64, 0x2a, 0x30, 0x71, 0x33, 0x4b, 0xed, 0xf7, 0xcb, 0x3a,
0xed, 0xe7, 0x15, 0x72, 0x9f, 0x93, 0xe6, 0xeb, 0xdc, 0x42, 0xf3, 0xc9, 0x70, 0xbf, 0x52, 0x7e, 0x68, 0x61, 0xd4,
0x94, 0x5f, 0x5e, 0x57, 0x7e, 0x85, 0xa7, 0xc2, 0x5b, 0xfd, 0x2e, 0x0a, 0x5d, 0xd4, 0xe7, 0x60, 0x08, 0xe9, 0x47,
0x70, 0x0d, 0x0d, 0x1e, 0x14, 0xc9, 0x62, 0xb1, 0x76, 0x41, 0x5c, 0x1f, 0x73, 0xaa, 0x1d, 0xca, 0x18, 0x23, 0x9e,
0x96, 0x1c, 0x24, 0x19, 0x1c, 0x8c, 0xdf, 0xc0, 0x80, 0x98, 0x94, 0x84, 0x74, 0x08, 0x9d, 0xb5, 0x99, 0x88, 0xca,
0x5d, 0xbc, 0xd9, 0xb8, 0xac, 0x29, 0x14, 0x61, 0x27, 0x98, 0xa9, 0x94, 0x0a, 0x02, 0x69, 0xf2, 0xdd, 0xe9, 0xd4,
0x82, 0xa1, 0x85, 0x6b, 0x2a, 0x20, 0xaf, 0xed, 0x7a, 0xd0, 0xe4, 0x3d, 0xc5, 0xd0, 0xd7, 0xa9, 0x11, 0x2f, 0x33,
0xf8, 0x1a, 0x36, 0x7f, 0x4d, 0x94, 0xe4, 0x21, 0x13, 0xb1, 0x57, 0xf0, 0x89, 0x90, 0x4d, 0xc1, 0xce, 0x04, 0xfa,
0xa1, 0x5d, 0xd9, 0x4b, 0x77, 0x8b, 0xca, 0xa5, 0x45, 0x63, 0x2b, 0x51, 0xb3, 0xe6, 0x87, 0xf1, 0x66, 0x0a, 0xfb,
0xd9, 0xa3, 0x04, 0x02, 0xd2, 0x54, 0x4e, 0x52, 0xcd, 0x7b, 0x98, 0x0e, 0x01, 0x24, 0xd8, 0xfd, 0x04, 0x16, 0xfa,
0x4d, 0x89, 0x09, 0x16, 0x55, 0x63, 0xb7, 0x39, 0x68, 0xcd, 0x39, 0x69, 0xbe, 0x39, 0x6a, 0xed, 0x4d, 0x65, 0x3d,
0x63, 0x76, 0x80, 0x6d, 0xbb, 0x9b, 0xc5, 0x61, 0xba, 0xd9, 0x19, 0x1a, 0x82, 0x0b, 0x8f, 0xff, 0x93, 0x12, 0xd3,
0x40, 0x72, 0xa9, 0x1b, 0x3f, 0xa1, 0x0e, 0xc3, 0xff, 0x96, 0xa4, 0x80, 0x07, 0xb5, 0xd5, 0x58, 0x71, 0xee, 0x15,
0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, 0x89, 0x62, 0x9e, 0x13, 0x00, 0xa3, 0xd8, 0xfc,
0x29, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4d, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0x4f, 0xe8, 0x48, 0x45, 0x65, 0x73, 0x12,
0xf3, 0x6f, 0x0b, 0x30, 0xcd, 0x89, 0x0f, 0xf5, 0x5c, 0xc3, 0x50, 0x80, 0xaf, 0x6c, 0x28, 0x35, 0xdb, 0xe3, 0xdf,
0x3b, 0xdb, 0x7d, 0x49, 0x14, 0xc1, 0x02, 0x0d, 0xba, 0x5c, 0x81, 0x2f, 0x60, 0x19, 0xdc, 0x92, 0x7e, 0x0a, 0xbe,
0x97, 0x57, 0xc1, 0x67, 0xec, 0x7f, 0x01, 0x68, 0x55, 0x60, 0x40, 0xb9, 0xd3, 0x34, 0xac, 0x84, 0xb8, 0x44, 0x85,
0x59, 0xc5, 0xf9, 0xe3, 0x3a, 0xaf, 0x9b, 0x96, 0x25, 0x06, 0xe5, 0xe7, 0xae, 0xe1, 0xc6, 0xf7, 0x1a, 0xf9, 0xe3,
0x7b, 0xcf, 0x41, 0xb7, 0x13, 0x69, 0xef, 0xde, 0xcd, 0xef, 0x90, 0x85, 0x86, 0xf7, 0xc2, 0xe6, 0xd0, 0x16, 0xe9,
0x92, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x07, 0x0c, 0xc1, 0x63, 0xa7, 0x32, 0xf9,
0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, 0xe4, 0xc5, 0x3a, 0xd9, 0x9e, 0x9d, 0xc2, 0xf3,
0xcb, 0xb8, 0x00, 0xea, 0x00, 0xfa, 0x15, 0x95, 0xc5, 0x06, 0x72, 0x71, 0x53, 0xd6, 0x7a, 0x45, 0xe3, 0xf1, 0xb5,
0x5d, 0x58, 0x5d, 0x81, 0x4f, 0xa3, 0x74, 0x9c, 0x88, 0x49, 0xcc, 0xa4, 0xca, 0x35, 0xb9, 0x36, 0xba, 0x97, 0xb6,
0x68, 0x9e, 0x0b, 0x09, 0x5e, 0x11, 0xb8, 0x21, 0xf4, 0x95, 0xbe, 0x5c, 0x6f, 0xa0, 0xe0, 0x51, 0x7b, 0x73, 0x11,
0x4c, 0x4c, 0x3c, 0x66, 0x48, 0x4d, 0xbf, 0x0e, 0xa7, 0x56, 0x16, 0x2b, 0x0e, 0xbf, 0xce, 0x19, 0x6b, 0x28, 0x00,
0xe2, 0x93, 0x07, 0x57, 0xbb, 0x49, 0xaf, 0x94, 0x76, 0x50, 0x1a, 0x21, 0xbe, 0xad, 0xf0, 0x75, 0x97, 0x8a, 0xaf,
0x5c, 0x75, 0xef, 0x6b, 0xc6, 0x8c, 0x0b, 0x46, 0xcf, 0xf9, 0x2c, 0x69, 0x5c, 0xbb, 0xa1, 0xbb, 0x3a, 0x3f, 0x7a,
0x3f, 0xc8, 0xbc, 0x85, 0x19, 0xb0, 0x09, 0xa8, 0x82, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x3b, 0xf3, 0x88, 0x57,
0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xbb, 0xc1, 0x77, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26, 0xca,
0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54, 0x44,
0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d, 0x46,
0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85, 0x77,
0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0, 0x74,
0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x75, 0xbe, 0x6a, 0x95, 0x58, 0xba,
0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f, 0xcd,
0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45, 0x90,
0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa, 0x66,
0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97, 0x97,
0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x98, 0xad, 0x1f, 0xbc,
0xb9, 0x7b, 0x05, 0x2c, 0xc7, 0x80, 0xdd, 0x31, 0x73, 0x1a, 0x43, 0x55, 0x1b, 0xf8, 0x87, 0xf5, 0x83, 0xad, 0xdb,
0xc3, 0x3f, 0x0c, 0x7e, 0x08, 0xae, 0x6d, 0x6c, 0x6c, 0xe3, 0xed, 0x5a, 0x22, 0xc8, 0x2b, 0x3c, 0xd0, 0xc7, 0xab,
0x8f, 0x82, 0x96, 0xeb, 0xc4, 0xf6, 0xc0, 0xa1, 0xb0, 0x35, 0xc8, 0x37, 0x29, 0x93, 0x46, 0x8b, 0x82, 0x67, 0x33,
0x39, 0x43, 0x21, 0xaf, 0xf9, 0x38, 0x68, 0x3b, 0xc2, 0xdf, 0xc0, 0xa9, 0x1d, 0x2f, 0x2f, 0x3f, 0x41, 0x1f, 0xf0,
0x74, 0xa5, 0x34, 0x15, 0x71, 0x4a, 0xb9, 0x45, 0x57, 0xeb, 0x3c, 0x18, 0x29, 0x2e, 0xa6, 0xa8, 0x74, 0xdc, 0xe5,
0xb5, 0xb3, 0x91, 0xd3, 0x5f, 0xe2, 0xd5, 0x45, 0xba, 0x7c, 0x24, 0xb2, 0x55, 0x4b, 0xef, 0x37, 0x7d, 0xba, 0x6d,
0xcf, 0x18, 0x9f, 0x66, 0x63, 0x3a, 0x98, 0xf1, 0x71, 0x22, 0xbc, 0x3e, 0x31, 0xd6, 0x77, 0x8b, 0xc0, 0x74, 0x73,
0x6c, 0xf2, 0xc3, 0xf1, 0x7a, 0xb3, 0x59, 0xe3, 0x0e, 0xde, 0x38, 0x4f, 0x9c, 0x65, 0x89, 0x11, 0x95, 0xa5, 0x86,
0x07, 0xb4, 0x42, 0xdc, 0xbc, 0x67, 0x02, 0xe3, 0xb2, 0x0b, 0x92, 0xda, 0x6e, 0x20, 0x70, 0xb1, 0x27, 0x31, 0x4b,
0xc6, 0xb6, 0x07, 0xe5, 0x81, 0xbe, 0x18, 0x4d, 0xb7, 0x80, 0x69, 0x79, 0xed, 0xec, 0x2c, 0xb5, 0xbd, 0x6a, 0xaa,
0x00, 0x66, 0xc9, 0xf2, 0xf8, 0x04, 0x59, 0xf7, 0x5b, 0xe8, 0x22, 0x06, 0x8c, 0x8d, 0x2b, 0x73, 0xee, 0x72, 0xdd,
0x8a, 0xf8, 0x46, 0x13, 0x69, 0x52, 0x1f, 0x52, 0xdf, 0x61, 0x58, 0xab, 0xab, 0x1c, 0x24, 0x70, 0x8f, 0xbc, 0x5b,
0xe2, 0xd2, 0xd3, 0x67, 0x16, 0x93, 0x2a, 0x7d, 0x4b, 0x5d, 0x8b, 0x6b, 0x86, 0xbd, 0xe2, 0x01, 0xd8, 0x1f, 0x18,
0xb7, 0x88, 0x45, 0xbc, 0x9d, 0xd7, 0x52, 0x58, 0x1b, 0x73, 0xa0, 0xb9, 0xe1, 0x06, 0xbf, 0xb1, 0x6a, 0xcd, 0xc0,
0x0c, 0x33, 0xce, 0x48, 0x7e, 0x33, 0xee, 0x55, 0x4d, 0x1c, 0xb9, 0x0a, 0x20, 0xfa, 0x96, 0x74, 0x49, 0x0e, 0xaf,
0x64, 0xb9, 0xea, 0x0c, 0xf9, 0x57, 0x58, 0x67, 0xbd, 0x38, 0x01, 0x33, 0x69, 0xca, 0x4b, 0x4c, 0x4c, 0x11, 0x97,
0x9b, 0x65, 0xcc, 0xd3, 0xf4, 0x59, 0xb4, 0x83, 0x93, 0x1b, 0x09, 0x1c, 0xb1, 0x6f, 0x2c, 0x43, 0x33, 0x61, 0x23,
0x26, 0xd2, 0xa8, 0x94, 0x12, 0x3e, 0x90, 0x4b, 0x2d, 0xf9, 0xcb, 0x5c, 0x5e, 0x7d, 0xb9, 0x4d, 0x70, 0x40, 0x5e,
0x03, 0xcb, 0xa1, 0x71, 0xdc, 0x32, 0x90, 0x88, 0xc5, 0x80, 0x18, 0xb5, 0x2a, 0x57, 0x93, 0x51, 0x9d, 0xcc, 0x57,
0xc8, 0x85, 0x8a, 0x3c, 0xb8, 0x25, 0x50, 0xf2, 0xe7, 0x98, 0x3a, 0x98, 0x95, 0xda, 0x4d, 0x8b, 0x4d, 0x92, 0xf7,
0xcc, 0x80, 0xe4, 0xfa, 0x6b, 0x78, 0x68, 0xfc, 0xe2, 0x95, 0x39, 0x25, 0x7c, 0x51, 0xc6, 0xd2, 0xd2, 0x98, 0x4b,
0xff, 0x42, 0xde, 0xa7, 0x95, 0x80, 0xfd, 0x0a, 0x62, 0xca, 0xc0, 0x25, 0x36, 0x2e, 0x48, 0xca, 0x6b, 0x79, 0xca,
0xee, 0x6b, 0x28, 0xdf, 0x15, 0x93, 0xae, 0x52, 0x59, 0x57, 0x58, 0x75, 0xbf, 0x2e, 0x58, 0x7e, 0xb1, 0xcf, 0x30,
0x37, 0x19, 0x0d, 0xb2, 0x15, 0x33, 0x9b, 0xf2, 0xab, 0xbd, 0x6b, 0xbf, 0xf2, 0x50, 0xd2, 0xa1, 0x5a, 0xa5, 0x9b,
0x57, 0x6e, 0x38, 0xc6, 0x8d, 0x1b, 0x8e, 0x00, 0x36, 0x86, 0x9d, 0x2a, 0x52, 0xeb, 0xfc, 0xf7, 0xd5, 0xf0, 0x13,
0xed, 0xb5, 0xa1, 0xde, 0x75, 0xc3, 0xb5, 0xe9, 0xe9, 0xd7, 0xa0, 0x6a, 0x64, 0x09, 0x5d, 0x87, 0x2a, 0x26, 0x23,
0x51, 0x62, 0xba, 0x4a, 0x79, 0xd4, 0xd7, 0x88, 0x73, 0x10, 0x37, 0x94, 0xbf, 0xf8, 0xe7, 0xf0, 0xe2, 0x28, 0x40,
0x23, 0x6a, 0x39, 0xc9, 0x52, 0xde, 0x9a, 0x44, 0xb3, 0x38, 0xb9, 0x08, 0x16, 0x71, 0x6b, 0x96, 0xa5, 0x59, 0x31,
0x07, 0xae, 0xf4, 0x8a, 0x0b, 0xb0, 0xe1, 0x67, 0xad, 0x45, 0xec, 0x3d, 0x67, 0xc9, 0x29, 0xe3, 0xf1, 0x28, 0xf2,
0xec, 0xbd, 0x1c, 0xc4, 0x83, 0xf5, 0x3a, 0xca, 0xf3, 0xec, 0xcc, 0xf6, 0xde, 0x65, 0xc7, 0xc0, 0xb4, 0xde, 0x9b,
0xf3, 0x8b, 0x13, 0x96, 0x7a, 0xef, 0x8f, 0x17, 0x29, 0x5f, 0x78, 0x45, 0x94, 0x16, 0xad, 0x82, 0xe5, 0xf1, 0x04,
0xd4, 0x44, 0x92, 0xe5, 0x2d, 0xcc, 0x7f, 0x9e, 0xb1, 0x20, 0x89, 0x4f, 0xa6, 0xdc, 0x1a, 0x47, 0xf9, 0xa7, 0x5e,
0xab, 0x35, 0xcf, 0xe3, 0x59, 0x94, 0x5f, 0xb4, 0xa8, 0x45, 0xf0, 0x45, 0x7b, 0x3b, 0xfa, 0x6a, 0x72, 0xbf, 0xc7,
0x73, 0xe8, 0x1b, 0x23, 0x15, 0x03, 0x10, 0x3e, 0xd6, 0xf6, 0x4e, 0x7b, 0x56, 0xdc, 0x11, 0x27, 0x4a, 0x51, 0xca,
0xcb, 0x23, 0xef, 0x23, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79, 0x01,
0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04, 0xf7,
0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66, 0x05,
0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2, 0x93,
0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff, 0x9d,
0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39, 0xf8,
0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe, 0x5e,
0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01, 0xc8,
0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e, 0x8e,
0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f, 0xc5,
0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98, 0xe8,
0xc3, 0x55, 0x1f, 0xb1, 0xf9, 0xfa, 0xc6, 0x2f, 0xd5, 0x78, 0x67, 0x2a, 0x6f, 0x50, 0xa8, 0x08, 0xf5, 0xcd, 0x16,
0xcc, 0x78, 0xcb, 0xfb, 0x1d, 0x7d, 0x50, 0x35, 0xf8, 0x9a, 0x91, 0xd6, 0x0b, 0xb8, 0x67, 0xe6, 0x02, 0xf5, 0xd2,
0x3e, 0x86, 0xa4, 0x5a, 0x2d, 0x17, 0xf4, 0x06, 0xc3, 0x10, 0x12, 0x1d, 0x08, 0x3a, 0xf9, 0xa0, 0xa0, 0x6f, 0x6a,
0x64, 0x6e, 0x50, 0x38, 0x99, 0x0b, 0x5b, 0x3e, 0xd3, 0x72, 0x1d, 0x94, 0x34, 0x78, 0xd9, 0x1f, 0x98, 0x6c, 0x00,
0xd2, 0x9b, 0x42, 0x5d, 0x7f, 0x09, 0x85, 0x2b, 0xa5, 0x1c, 0xa9, 0xd9, 0x75, 0x57, 0xf4, 0x61, 0x55, 0x62, 0xca,
0x48, 0x3e, 0x1c, 0xfe, 0x3b, 0x0c, 0x7b, 0x47, 0x3b, 0x96, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0xa9, 0x5b, 0xf5, 0xf8,
0x6d, 0x52, 0xb8, 0xb6, 0xc7, 0xb4, 0x9c, 0x47, 0x37, 0xb8, 0xf6, 0x91, 0x03, 0x4e, 0x87, 0x20, 0xe2, 0x8e, 0x81,
0x8c, 0x72, 0x28, 0x08, 0x51, 0x75, 0x8d, 0x29, 0xdf, 0x8d, 0xee, 0x5f, 0xfa, 0x8b, 0x34, 0x06, 0x49, 0xf7, 0x31,
0x1e, 0xd3, 0xbd, 0x93, 0x78, 0x4c, 0x07, 0x11, 0x2d, 0x4a, 0x3c, 0xc2, 0xc8, 0x36, 0x14, 0xa8, 0xef, 0xb0, 0xc0,
0xb3, 0x4c, 0x64, 0xb1, 0x5b, 0x36, 0x1e, 0x26, 0x18, 0xaa, 0x72, 0x9c, 0xcd, 0xa2, 0x38, 0x0d, 0xf0, 0xfb, 0x20,
0x9e, 0x1e, 0x31, 0xc0, 0x2e, 0x1e, 0xfc, 0x64, 0x32, 0x17, 0xad, 0xe3, 0xfa, 0xbf, 0x80, 0x1c, 0xa1, 0xfe, 0xa5,
0xf4, 0xc3, 0x34, 0x5c, 0xea, 0x98, 0xb7, 0x5e, 0x0a, 0xb2, 0x87, 0x2b, 0x9b, 0x95, 0x51, 0x9c, 0x63, 0x97, 0xd3,
0x8f, 0x41, 0xab, 0x13, 0x74, 0xb4, 0xeb, 0x5a, 0xbb, 0x8d, 0x2a, 0x72, 0x59, 0xe4, 0x8d, 0x46, 0x82, 0x41, 0x3f,
0x0b, 0x38, 0xab, 0x77, 0x0d, 0xab, 0x27, 0xf9, 0x12, 0x03, 0x38, 0x27, 0xa9, 0x53, 0x03, 0x82, 0xce, 0x02, 0xae,
0x98, 0xca, 0x2d, 0x23, 0x52, 0x4a, 0x8f, 0x69, 0x03, 0xd7, 0xef, 0x12, 0xe1, 0xbd, 0xa1, 0x7a, 0x0a, 0x94, 0x62,
0xb9, 0xf1, 0xd1, 0xae, 0xd8, 0xf1, 0x16, 0xf1, 0x58, 0x68, 0xc3, 0x16, 0xb4, 0xad, 0x3f, 0x8d, 0x80, 0x4a, 0x9f,
0x42, 0x7b, 0x63, 0xe9, 0xa8, 0xc4, 0xfa, 0x1c, 0xe6, 0xda, 0x13, 0x5a, 0x8f, 0x6e, 0xe4, 0xdb, 0xfd, 0x8d, 0x25,
0x2f, 0x77, 0xb7, 0x44, 0xef, 0xfe, 0x51, 0x59, 0x90, 0x82, 0x32, 0x03, 0x69, 0xd5, 0x14, 0xa2, 0x0e, 0x86, 0xa5,
0xf4, 0x5d, 0x1c, 0x37, 0xd7, 0x46, 0x97, 0x88, 0x18, 0x4b, 0xb6, 0x2b, 0x30, 0x5d, 0x29, 0xca, 0x61, 0x4f, 0xea,
0x84, 0x94, 0x42, 0xe4, 0x60, 0xf4, 0x56, 0xa1, 0x38, 0x42, 0x08, 0x06, 0x1b, 0xcb, 0xb8, 0x0c, 0x37, 0x96, 0x59,
0x79, 0x04, 0x96, 0x09, 0x42, 0x95, 0xab, 0xcf, 0xbb, 0xc0, 0xc4, 0x22, 0xc8, 0x62, 0xd1, 0x08, 0x38, 0x2d, 0x2b,
0x6d, 0x6b, 0x20, 0xa0, 0x01, 0x0f, 0x10, 0x0b, 0xc0, 0x76, 0xa3, 0x5e, 0x0c, 0x70, 0x11, 0xad, 0xfb, 0x30, 0xd0,
0xee, 0x96, 0x68, 0x04, 0x78, 0xe5, 0x08, 0x72, 0x85, 0x16, 0xa6, 0xe3, 0x98, 0xa8, 0x8d, 0xe3, 0x53, 0x4d, 0x3a,
0xca, 0x4d, 0xee, 0xef, 0x26, 0xd1, 0x31, 0x4b, 0x60, 0xc8, 0xe2, 0xf2, 0xb2, 0x0d, 0x23, 0x89, 0x57, 0x6b, 0x37,
0x4e, 0xe7, 0x0b, 0xf9, 0x99, 0x2d, 0x98, 0xb8, 0x83, 0x27, 0x9f, 0x78, 0x0b, 0x60, 0xa0, 0x8e, 0xf2, 0x02, 0x39,
0x00, 0x80, 0x48, 0xa7, 0x08, 0x08, 0x5d, 0xc5, 0x16, 0x50, 0x1a, 0x8f, 0x57, 0xcb, 0x60, 0x27, 0xce, 0xb1, 0x34,
0x85, 0xe7, 0x59, 0x9c, 0xe2, 0x63, 0x81, 0x8f, 0xd1, 0x39, 0x3e, 0x66, 0xf0, 0xa8, 0x71, 0xcf, 0x4b, 0xfb, 0x6f,
0xb2, 0x02, 0x96, 0x26, 0x40, 0x76, 0x79, 0x09, 0xf2, 0x5e, 0x93, 0x60, 0x77, 0x0b, 0x88, 0x85, 0x9c, 0x22, 0x3e,
0xba, 0xc2, 0x4c, 0x32, 0xb2, 0x62, 0xde, 0x12, 0xe5, 0x16, 0x69, 0xd5, 0x10, 0x9c, 0xae, 0xdc, 0x69, 0x18, 0x0f,
0x9e, 0x4c, 0x2f, 0x79, 0x82, 0x2f, 0xae, 0x6d, 0x89, 0xaf, 0x62, 0x08, 0xa2, 0xd0, 0x23, 0x62, 0xa8, 0xcb, 0xb8,
0xfc, 0xde, 0x4d, 0x1c, 0xda, 0x38, 0x0b, 0xd8, 0x6f, 0xa8, 0x05, 0x78, 0x14, 0x27, 0xa2, 0xf1, 0x1a, 0x7c, 0x1a,
0x79, 0x82, 0x84, 0xce, 0xee, 0x56, 0x05, 0x1b, 0x00, 0x3f, 0x12, 0xb7, 0xac, 0x1d, 0x91, 0x03, 0x69, 0x8b, 0x72,
0x3a, 0x3b, 0x97, 0x5b, 0x5a, 0x46, 0x76, 0x45, 0xac, 0x5c, 0xa3, 0x4a, 0x39, 0x8b, 0xf6, 0x24, 0x4a, 0xd7, 0x35,
0x05, 0xe8, 0xe7, 0x8c, 0x8d, 0x3d, 0xdb, 0x02, 0x59, 0x2a, 0x9e, 0x3f, 0x26, 0xec, 0x94, 0xc9, 0x2f, 0xa5, 0xe8,
0x41, 0x74, 0xe5, 0x08, 0x54, 0x32, 0x97, 0x97, 0x38, 0x25, 0x7b, 0x2a, 0x1c, 0x25, 0x25, 0xea, 0x88, 0x78, 0xb6,
0x31, 0x68, 0x73, 0x8e, 0x76, 0x7d, 0x58, 0xaf, 0x03, 0xd6, 0xae, 0x2d, 0xe0, 0x25, 0x3b, 0xee, 0x66, 0xe4, 0x60,
0x00, 0x36, 0x99, 0xc0, 0x76, 0x51, 0x91, 0x65, 0x2d, 0x0b, 0x04, 0x54, 0xe0, 0x94, 0x7a, 0xb6, 0x68, 0x61, 0x57,
0x6d, 0xf5, 0x93, 0x24, 0x4e, 0x92, 0x8d, 0x3e, 0xad, 0x99, 0x0b, 0xb8, 0x63, 0x43, 0x44, 0x5a, 0x1b, 0xf2, 0xcd,
0xfe, 0xb7, 0x7f, 0xfe, 0x9f, 0xff, 0x15, 0x06, 0xa6, 0x7e, 0x6e, 0x69, 0x5d, 0xdd, 0xea, 0x7f, 0x40, 0xab, 0x45,
0x7a, 0x43, 0xbb, 0xbf, 0xfe, 0xe3, 0x7f, 0x83, 0x66, 0x74, 0x23, 0xc7, 0x2d, 0x8f, 0x08, 0xa2, 0x11, 0x1a, 0x41,
0x9f, 0x05, 0x52, 0x6d, 0x90, 0x2b, 0x67, 0xfa, 0x27, 0x04, 0xbb, 0xe0, 0xd9, 0xfc, 0x5a, 0x70, 0x10, 0xea, 0x51,
0x92, 0x15, 0x4c, 0xc3, 0x23, 0x64, 0xed, 0xe7, 0x01, 0x44, 0x73, 0xcd, 0x81, 0xcb, 0x0b, 0x4b, 0x8f, 0x23, 0x96,
0x67, 0xdd, 0x38, 0x8d, 0xd5, 0x2b, 0x18, 0x27, 0x74, 0x28, 0xae, 0x00, 0xeb, 0x25, 0x9e, 0xe0, 0x81, 0x04, 0x82,
0x5b, 0xff, 0xca, 0xd7, 0xfa, 0xc1, 0x34, 0x7f, 0x8a, 0xb1, 0x44, 0x28, 0x45, 0x8d, 0x00, 0x3f, 0x41, 0x68, 0x7d,
0xd4, 0xcf, 0xd1, 0xb9, 0x7e, 0x46, 0xc1, 0x26, 0x26, 0x00, 0x5d, 0x34, 0x43, 0x33, 0xc3, 0x9c, 0x41, 0xa4, 0x01,
0x54, 0x7e, 0xa4, 0x91, 0x4d, 0x22, 0x84, 0xd7, 0x47, 0x4c, 0xba, 0xc4, 0x2b, 0x36, 0x8b, 0x9c, 0x7d, 0x4c, 0xb2,
0x33, 0x0c, 0x4e, 0x21, 0x91, 0xae, 0xaa, 0x2f, 0xff, 0xf5, 0x5f, 0x7c, 0xff, 0x5f, 0xff, 0xe5, 0x8a, 0x06, 0x53,
0xd8, 0x07, 0x60, 0x4d, 0xf2, 0x50, 0xd3, 0xb9, 0x81, 0xd6, 0xfa, 0x41, 0x11, 0xcf, 0xf5, 0x35, 0x12, 0x71, 0x2c,
0x95, 0x78, 0xcb, 0x47, 0x42, 0x5b, 0x33, 0xc5, 0xcd, 0xb3, 0x20, 0x64, 0x57, 0x4c, 0x83, 0x55, 0x37, 0xcc, 0x73,
0xe4, 0x06, 0xd7, 0xd0, 0xe5, 0x33, 0x31, 0x5e, 0x0f, 0xc6, 0x8d, 0x10, 0x78, 0x20, 0xfd, 0x85, 0x7e, 0x78, 0x22,
0xa4, 0x7b, 0x20, 0x96, 0x41, 0xca, 0xfa, 0x1a, 0x40, 0x9e, 0x75, 0x40, 0x13, 0x50, 0x93, 0xb8, 0xd2, 0xad, 0x64,
0x8a, 0x1c, 0xe7, 0xfd, 0x57, 0x78, 0xab, 0xce, 0xc2, 0xde, 0xa8, 0x59, 0x0b, 0x32, 0x04, 0x38, 0x19, 0x02, 0x52,
0x03, 0x99, 0x3a, 0x18, 0x3d, 0x8c, 0x6c, 0xbd, 0xae, 0xfd, 0x88, 0xdd, 0x6b, 0xda, 0x92, 0x4b, 0x6d, 0x19, 0x4b,
0x4b, 0x56, 0x6a, 0x4b, 0xfc, 0x76, 0x4a, 0x43, 0x5b, 0xc6, 0x57, 0x6a, 0x4b, 0xa4, 0xdc, 0x00, 0x47, 0x0e, 0xed,
0x4d, 0x0c, 0xa3, 0x18, 0xba, 0x99, 0xa3, 0x5d, 0x02, 0xbe, 0xf3, 0xe8, 0x93, 0x34, 0x4b, 0x08, 0x01, 0x8c, 0x23,
0x68, 0x43, 0x4b, 0x60, 0x00, 0x2a, 0xf6, 0xa8, 0xd4, 0x9b, 0x1e, 0x1f, 0x8d, 0x09, 0xb8, 0xbb, 0x9c, 0x30, 0x14,
0xc9, 0x47, 0x5b, 0x38, 0x84, 0xd8, 0x2b, 0x25, 0x3d, 0x03, 0x52, 0x5b, 0x38, 0xce, 0x91, 0xb7, 0x14, 0x01, 0xa9,
0xc0, 0x7e, 0xfb, 0x66, 0xff, 0xc0, 0xf6, 0x8e, 0xb3, 0xf1, 0x45, 0x60, 0x83, 0x33, 0x01, 0x86, 0x87, 0xeb, 0xf3,
0x29, 0x4b, 0x1d, 0x65, 0xce, 0x67, 0x09, 0xb8, 0x33, 0xd9, 0x89, 0xf8, 0x7e, 0x42, 0x33, 0x98, 0x0e, 0x34, 0xa5,
0x0f, 0x2c, 0xf6, 0x77, 0xb9, 0xf8, 0xf6, 0x28, 0xcf, 0xf1, 0xb1, 0x8f, 0xe9, 0x04, 0xbb, 0x5b, 0xf0, 0x80, 0x2f,
0xfb, 0xa8, 0x8a, 0xf4, 0x9b, 0x80, 0xb3, 0x10, 0xef, 0x5b, 0xd8, 0x7e, 0x4b, 0xf5, 0x45, 0x28, 0xfa, 0x92, 0xd1,
0xb4, 0xc5, 0x5d, 0x99, 0x71, 0x34, 0xf6, 0x18, 0xad, 0x34, 0xb2, 0xb8, 0x81, 0x1a, 0x7c, 0xac, 0x4b, 0x84, 0xe6,
0x37, 0x8a, 0x68, 0x94, 0x4a, 0x4f, 0xcb, 0x2a, 0x9c, 0x90, 0x2c, 0x3b, 0x31, 0x19, 0xfc, 0x24, 0xf0, 0x8f, 0xcc,
0x6f, 0x85, 0x89, 0xcf, 0xfa, 0x68, 0x24, 0x0f, 0xff, 0xec, 0x7d, 0x64, 0xde, 0xc5, 0x11, 0xb5, 0x54, 0x8e, 0x29,
0x46, 0x4c, 0xd0, 0x81, 0x6f, 0xab, 0x08, 0x04, 0x98, 0x26, 0x49, 0x34, 0x2f, 0x58, 0xa0, 0x1e, 0xa4, 0x8f, 0x8a,
0xae, 0xee, 0x6a, 0x50, 0xc0, 0x34, 0x61, 0x4a, 0x3e, 0x5d, 0x9a, 0x4e, 0xec, 0x03, 0x70, 0x62, 0x31, 0x01, 0xbf,
0x15, 0x81, 0xe2, 0x4d, 0x83, 0x84, 0x4d, 0x78, 0xc9, 0xf1, 0x86, 0xf7, 0x52, 0x45, 0x0d, 0xfc, 0xee, 0x0e, 0x38,
0xb6, 0x96, 0x8f, 0xff, 0xdf, 0x34, 0xf6, 0x38, 0x48, 0xc1, 0x11, 0xa5, 0x2b, 0x1f, 0x78, 0x9d, 0x0e, 0x20, 0x32,
0xdf, 0x97, 0xc6, 0x44, 0x23, 0x86, 0x11, 0x95, 0x92, 0xe7, 0x20, 0xb2, 0x3d, 0x9e, 0x9b, 0xed, 0x40, 0xd4, 0xae,
0x84, 0x55, 0x56, 0x1d, 0xfb, 0x6d, 0x57, 0x9a, 0xff, 0xab, 0x8d, 0x55, 0x74, 0xa4, 0xfe, 0xb6, 0x42, 0x21, 0x23,
0x8e, 0x53, 0x0a, 0x2d, 0xb3, 0x14, 0x3d, 0x4c, 0x9c, 0x56, 0x23, 0x3c, 0x37, 0x1a, 0x89, 0x25, 0xed, 0xf8, 0x43,
0xda, 0xf1, 0x24, 0xc1, 0x86, 0x4b, 0x31, 0xf7, 0x28, 0x4a, 0x46, 0x0e, 0x02, 0x60, 0xb5, 0xac, 0x47, 0x40, 0x4d,
0x57, 0x45, 0x19, 0xfc, 0x87, 0x48, 0xdc, 0x52, 0xc8, 0xbb, 0x35, 0x54, 0x3a, 0x1a, 0x96, 0x65, 0xef, 0x8c, 0x39,
0x87, 0xbf, 0xc9, 0x0b, 0x03, 0xe2, 0x9e, 0xa8, 0xfe, 0xd6, 0x5e, 0xbb, 0x74, 0x87, 0xde, 0x5f, 0x8c, 0x0f, 0x98,
0xd9, 0x8a, 0xa1, 0x6d, 0x0f, 0x96, 0xe1, 0x2f, 0x21, 0xf6, 0x7d, 0xe5, 0xd8, 0x68, 0x55, 0x52, 0xcd, 0x45, 0x8b,
0xf8, 0xcb, 0xc6, 0x6e, 0x22, 0xdc, 0xfd, 0xfd, 0x55, 0x51, 0x8b, 0x6f, 0x6e, 0x8e, 0x5a, 0xb0, 0x5b, 0x46, 0x2d,
0xbe, 0xf9, 0x83, 0xa3, 0x16, 0xdf, 0x37, 0xa3, 0x16, 0xbf, 0x7e, 0x4e, 0xd4, 0x22, 0xcf, 0xce, 0x8a, 0xb0, 0x23,
0x4f, 0xc9, 0x41, 0xe6, 0xfc, 0x6d, 0xc2, 0x17, 0x30, 0x51, 0x23, 0x78, 0x41, 0xd1, 0x0a, 0x91, 0xd8, 0x07, 0x92,
0x5d, 0xc6, 0x0a, 0xda, 0x3a, 0x83, 0xae, 0x75, 0x5f, 0x5d, 0x19, 0x02, 0x6f, 0xcd, 0xd5, 0x97, 0xdd, 0xba, 0x2a,
0x9a, 0x10, 0xd0, 0x37, 0x3f, 0x75, 0xc7, 0xee, 0xa6, 0x4a, 0xdd, 0x32, 0x47, 0xe8, 0xa9, 0x88, 0xbc, 0x60, 0x9f,
0xa5, 0xfd, 0x9f, 0x0e, 0x3b, 0xbd, 0xed, 0xce, 0x0c, 0x7a, 0x83, 0x0e, 0x85, 0xb7, 0x76, 0x6f, 0x7b, 0x1b, 0xdf,
0xce, 0xd4, 0x5b, 0x17, 0xdf, 0x62, 0xf5, 0xb6, 0x83, 0x6f, 0x23, 0xf5, 0xf6, 0x00, 0xdf, 0xc6, 0xea, 0xed, 0x21,
0xbe, 0x9d, 0xda, 0xe5, 0x21, 0xd7, 0xc0, 0x3d, 0x04, 0xbe, 0x22, 0x43, 0x3f, 0x50, 0x65, 0xb0, 0x69, 0xf1, 0xaa,
0x5d, 0x74, 0x12, 0xc4, 0x9e, 0x70, 0x88, 0x82, 0xdc, 0x3b, 0x03, 0xc9, 0x1f, 0x50, 0x66, 0xd9, 0x53, 0xfc, 0xe6,
0x02, 0xf8, 0x0f, 0x07, 0xf1, 0x8c, 0xa9, 0x8f, 0xcf, 0x2a, 0xac, 0xc1, 0x86, 0x3c, 0x6c, 0x0f, 0xcb, 0x9e, 0x5e,
0x27, 0x11, 0x2c, 0x51, 0x27, 0xf7, 0xb4, 0x72, 0x55, 0x9d, 0x98, 0xae, 0xa5, 0x57, 0xf8, 0x0a, 0x3d, 0x62, 0xb8,
0xd0, 0x13, 0xb0, 0x8d, 0x5a, 0xe7, 0xe0, 0x74, 0xad, 0xd5, 0x2d, 0x08, 0x91, 0xd6, 0x26, 0x84, 0x93, 0x7e, 0x3b,
0x88, 0x4e, 0xf4, 0xf3, 0x2b, 0x30, 0x76, 0xa3, 0x13, 0x76, 0x93, 0x9e, 0x21, 0x10, 0x4d, 0x1d, 0xa3, 0x80, 0x20,
0x6b, 0x08, 0x96, 0x06, 0x9d, 0x3f, 0xa9, 0x63, 0x90, 0x3a, 0x75, 0xad, 0x43, 0xd3, 0xd7, 0x8b, 0x80, 0xa2, 0x55,
0xc1, 0x2e, 0xd8, 0xdc, 0x54, 0x2a, 0x28, 0x0c, 0x15, 0x58, 0x70, 0xad, 0x2a, 0xd2, 0xfe, 0xf1, 0x95, 0x0a, 0xc9,
0x52, 0xba, 0xc8, 0x8c, 0xe4, 0xeb, 0x30, 0xfe, 0xaa, 0x78, 0xfc, 0xa2, 0x33, 0xc2, 0x3f, 0x52, 0xf8, 0x7e, 0x31,
0x99, 0x4c, 0xae, 0xd5, 0x4d, 0x5f, 0x8c, 0x27, 0xac, 0xcb, 0x76, 0x7a, 0x18, 0xe5, 0x6d, 0x49, 0x71, 0xd8, 0x29,
0x89, 0x76, 0xcb, 0xdb, 0x35, 0x46, 0xc9, 0x09, 0xea, 0xea, 0xf6, 0x4a, 0xac, 0x04, 0xaa, 0x2c, 0x41, 0x78, 0x9f,
0xc4, 0x69, 0xd0, 0x2e, 0xfd, 0x53, 0x29, 0xf5, 0xbf, 0x78, 0xf4, 0xe8, 0x51, 0xe9, 0x8f, 0xd5, 0x5b, 0x7b, 0x3c,
0x2e, 0xfd, 0xd1, 0x52, 0xa3, 0xd1, 0x6e, 0x4f, 0x26, 0xa5, 0x1f, 0xab, 0x82, 0xed, 0xee, 0x68, 0xbc, 0xdd, 0x2d,
0xfd, 0x33, 0xa3, 0x45, 0xe9, 0x33, 0xf9, 0x96, 0xb3, 0x71, 0x2d, 0x54, 0xfc, 0xb0, 0x0d, 0x95, 0x82, 0xd1, 0x96,
0xe8, 0xe0, 0x89, 0xc7, 0x20, 0x5a, 0xf0, 0x0c, 0x6c, 0xab, 0xb2, 0xc7, 0x40, 0x3e, 0x4f, 0xa4, 0x6c, 0x17, 0xdf,
0x76, 0x45, 0x89, 0xfe, 0xab, 0x29, 0xd1, 0x91, 0x99, 0x49, 0x9a, 0x33, 0xd2, 0x03, 0xcd, 0x6a, 0xe4, 0x2c, 0xaa,
0xfe, 0x35, 0x64, 0x95, 0xb0, 0x47, 0x69, 0x83, 0x2d, 0x85, 0x8c, 0xff, 0xf6, 0x2a, 0x19, 0xff, 0xdd, 0xcd, 0x32,
0xfe, 0xf8, 0x76, 0x22, 0xfe, 0xbb, 0x3f, 0x58, 0xc4, 0x7f, 0x6b, 0x8a, 0x78, 0x21, 0xc4, 0x2e, 0xc0, 0x7a, 0x25,
0xb3, 0xf5, 0x38, 0x3b, 0x6f, 0xe1, 0x96, 0xc8, 0x6d, 0x92, 0x9e, 0x1b, 0xb7, 0x12, 0xfe, 0x6b, 0x72, 0x7f, 0xd4,
0x60, 0xc6, 0x87, 0x62, 0x79, 0x76, 0x72, 0x92, 0x30, 0x25, 0xe3, 0x8d, 0x0a, 0xb2, 0x88, 0xdf, 0xa4, 0xa1, 0xfd,
0x06, 0x9c, 0x53, 0xa3, 0x64, 0x32, 0x81, 0xa2, 0xc9, 0xc4, 0x56, 0xb9, 0xb1, 0x20, 0xcf, 0xa8, 0xd5, 0xeb, 0x5a,
0x09, 0xb5, 0xfa, 0xfa, 0x6b, 0xb3, 0xcc, 0x2c, 0x90, 0x51, 0x28, 0xd3, 0x98, 0x90, 0x35, 0xe3, 0xb8, 0xc0, 0x3d,
0x58, 0x7d, 0xd8, 0x16, 0xed, 0x95, 0x19, 0x28, 0x95, 0x78, 0x84, 0x5f, 0x4c, 0x69, 0x7e, 0x44, 0x44, 0xe4, 0x31,
0xaf, 0x22, 0x57, 0x9d, 0x75, 0x1a, 0xdf, 0xab, 0xab, 0xce, 0x37, 0x61, 0xf1, 0x65, 0x2e, 0xc3, 0xe3, 0x8b, 0x17,
0x63, 0xe7, 0x02, 0xec, 0xd8, 0xb8, 0x78, 0x93, 0x36, 0x72, 0xc4, 0x04, 0xd8, 0x61, 0x68, 0x62, 0x5a, 0x0a, 0x82,
0x55, 0xc9, 0xf2, 0x55, 0x65, 0xcf, 0xe8, 0x24, 0x53, 0x89, 0x70, 0xc8, 0x41, 0x8d, 0x2c, 0x81, 0x39, 0x98, 0xd4,
0x85, 0xf4, 0xe1, 0x72, 0x91, 0x60, 0x71, 0x2a, 0xbf, 0x70, 0x4d, 0x91, 0xff, 0xa5, 0xd4, 0x1f, 0xf2, 0xe8, 0xbd,
0xea, 0x89, 0xc1, 0x76, 0x31, 0xc3, 0xb8, 0x54, 0x01, 0x76, 0x20, 0xdc, 0x1c, 0x3f, 0xc7, 0x23, 0x86, 0x50, 0x71,
0xec, 0x8a, 0x7a, 0xf8, 0xf9, 0x93, 0xea, 0xab, 0x90, 0xb5, 0x2f, 0x08, 0x36, 0x78, 0x00, 0xbf, 0xec, 0xcf, 0x51,
0x1b, 0x64, 0x0b, 0xee, 0x38, 0xd4, 0xca, 0x71, 0x4b, 0xaf, 0xbb, 0xd3, 0x06, 0x15, 0xe3, 0x8b, 0x6f, 0xfe, 0x38,
0xba, 0xb3, 0xc4, 0xf7, 0xaa, 0xb0, 0xf9, 0xca, 0x37, 0xb8, 0x34, 0x89, 0xf1, 0x23, 0x21, 0x02, 0x51, 0xe3, 0x9e,
0x88, 0x5a, 0xc4, 0xe6, 0xbb, 0xaf, 0xdc, 0x37, 0x83, 0xb0, 0xee, 0x3a, 0x0e, 0x96, 0x31, 0xb2, 0x7a, 0x21, 0xb6,
0x15, 0x56, 0xcd, 0x2a, 0x38, 0x37, 0xe8, 0xcc, 0xe2, 0xcc, 0x88, 0x39, 0xd7, 0xb6, 0x41, 0xa9, 0x82, 0xce, 0x22,
0x72, 0x7c, 0x81, 0xf1, 0x51, 0xe1, 0xfb, 0x2a, 0xa0, 0xeb, 0x5e, 0xa7, 0x01, 0x39, 0xfa, 0xa3, 0x9a, 0xd1, 0x55,
0x95, 0x2a, 0x28, 0xcd, 0x13, 0x02, 0x03, 0x19, 0x0a, 0xfe, 0xc2, 0x1a, 0xa7, 0x42, 0x6f, 0xc1, 0x34, 0x24, 0x80,
0xb5, 0x47, 0x86, 0x6e, 0x89, 0xad, 0xc0, 0x16, 0xd2, 0x02, 0x94, 0x1e, 0x76, 0xe8, 0x5b, 0x35, 0xd0, 0xd3, 0xd5,
0x98, 0xf1, 0x75, 0x4e, 0xda, 0xc5, 0x91, 0x5f, 0x9c, 0x79, 0xf0, 0xcf, 0xfa, 0x72, 0x09, 0x52, 0xfe, 0xf8, 0x53,
0xcc, 0xc1, 0xa6, 0x9e, 0xb7, 0x30, 0x02, 0x42, 0x21, 0x4c, 0xa9, 0x0e, 0xe9, 0xd8, 0x51, 0x5c, 0x0f, 0xea, 0x2d,
0x0a, 0xf4, 0xe5, 0xc8, 0x69, 0x09, 0xd2, 0x2c, 0x65, 0xbd, 0xfa, 0xf1, 0xb2, 0xe9, 0x37, 0x28, 0x62, 0x0d, 0x97,
0x19, 0xfa, 0x7e, 0xfc, 0x02, 0x7c, 0x3f, 0xa1, 0x46, 0xdb, 0xca, 0x69, 0x68, 0xaf, 0x6d, 0x1f, 0x48, 0xda, 0x6e,
0x92, 0xb5, 0x90, 0xaf, 0x3a, 0x47, 0x57, 0x39, 0x37, 0x37, 0x1d, 0xb6, 0x76, 0x77, 0x76, 0x3c, 0xf5, 0xcf, 0x38,
0xa5, 0x6e, 0x16, 0xd3, 0x61, 0xeb, 0x6d, 0x20, 0x0b, 0xa2, 0x09, 0x7e, 0x4d, 0xef, 0x36, 0x2d, 0x8f, 0x29, 0xd3,
0x71, 0x89, 0x6a, 0x3d, 0xe8, 0x3c, 0x02, 0x6f, 0xed, 0xd6, 0xc3, 0x5f, 0x8f, 0x7e, 0x29, 0x69, 0xa4, 0x2e, 0xac,
0xda, 0x76, 0x0f, 0xe5, 0x45, 0x12, 0x5d, 0x80, 0xd3, 0x48, 0x36, 0xc6, 0x31, 0x06, 0x70, 0x7b, 0xf3, 0x4c, 0x66,
0x0d, 0xe4, 0x2c, 0xa1, 0x5f, 0x59, 0x21, 0x97, 0x62, 0xfb, 0xc1, 0xfc, 0x5c, 0xad, 0x46, 0xa7, 0x91, 0x0d, 0xf0,
0x87, 0x1e, 0xfa, 0x5f, 0x9d, 0x65, 0x50, 0x3f, 0xb8, 0xde, 0x01, 0x18, 0x84, 0x61, 0xd3, 0xca, 0x05, 0x54, 0x6d,
0x28, 0x31, 0xd2, 0x1e, 0xaa, 0x81, 0x2c, 0x7f, 0x1b, 0x54, 0x65, 0x54, 0xb0, 0x1e, 0x7e, 0xd6, 0x30, 0x06, 0xd7,
0x54, 0x1a, 0x4f, 0xb3, 0x78, 0x3c, 0x4e, 0x58, 0x4f, 0xd9, 0x47, 0x56, 0xe7, 0x01, 0x66, 0x0d, 0x98, 0x4b, 0x56,
0x5f, 0x15, 0x83, 0x78, 0x9a, 0x4e, 0xd1, 0x31, 0xd8, 0x6b, 0xf8, 0x6d, 0xc2, 0xb5, 0xe4, 0x94, 0xc7, 0xe9, 0xed,
0x8a, 0x78, 0xf4, 0x5c, 0xc7, 0x65, 0x07, 0x8c, 0x45, 0x5a, 0xf0, 0x76, 0x8f, 0x67, 0xf3, 0xa0, 0xb5, 0x5d, 0x47,
0x04, 0xab, 0x34, 0x0a, 0xde, 0x1a, 0xb4, 0x3c, 0xb4, 0x0e, 0x84, 0x96, 0xb3, 0xfc, 0x8e, 0x2c, 0xa3, 0x01, 0xf0,
0xfb, 0x77, 0xba, 0xa8, 0xac, 0x23, 0xf3, 0xff, 0x67, 0xb7, 0x7c, 0xb5, 0x7e, 0xb7, 0x7c, 0xa5, 0x76, 0xcb, 0xf5,
0x1c, 0xfb, 0xc5, 0xa4, 0x83, 0x7f, 0x7a, 0x15, 0x42, 0xb0, 0x2a, 0x40, 0x0e, 0x0b, 0xed, 0xe2, 0x56, 0x17, 0xfe,
0xa3, 0xa1, 0xdb, 0x1e, 0xfe, 0xf1, 0xc1, 0x02, 0x6c, 0x5b, 0x58, 0x88, 0xff, 0xda, 0xb5, 0xaa, 0xce, 0x7d, 0xac,
0xc3, 0x5e, 0x3b, 0xab, 0x75, 0xdd, 0xeb, 0x37, 0x2d, 0xc8, 0x2b, 0xee, 0x04, 0x4a, 0x18, 0x83, 0xab, 0x16, 0x1d,
0x1f, 0x43, 0xe9, 0x24, 0x1b, 0x2d, 0x8a, 0xbf, 0x97, 0xf0, 0x4b, 0x22, 0x5e, 0xbb, 0xa5, 0x1b, 0xe3, 0xa8, 0xae,
0x22, 0x05, 0x45, 0x8d, 0xb0, 0xd4, 0xeb, 0x14, 0x14, 0xc0, 0x98, 0xcc, 0xe9, 0xfa, 0xf7, 0xd7, 0x6c, 0x82, 0xbf,
0xc9, 0xda, 0xac, 0x45, 0xe6, 0xdf, 0x4b, 0x8c, 0x6b, 0x89, 0xf0, 0x59, 0x34, 0x30, 0xd7, 0xb0, 0xfd, 0x68, 0x3d,
0xb8, 0x87, 0x6a, 0xa6, 0xa1, 0x52, 0x0a, 0x52, 0xef, 0x80, 0x17, 0x10, 0x2d, 0x12, 0x7e, 0xfd, 0xa8, 0x57, 0x71,
0xc6, 0xca, 0xa8, 0xd7, 0x08, 0xf4, 0xaa, 0xed, 0x2d, 0xa5, 0xf4, 0x17, 0x5f, 0xdd, 0xc7, 0x3f, 0x22, 0xf0, 0x75,
0x5c, 0xf9, 0x46, 0x22, 0x36, 0x80, 0xbe, 0xd1, 0x46, 0xcd, 0xf9, 0x11, 0x1a, 0x9c, 0xfc, 0x9f, 0xdb, 0xb6, 0x46,
0x63, 0xfd, 0x56, 0xcd, 0xa5, 0x55, 0xfa, 0x59, 0xad, 0x3f, 0x6f, 0xf0, 0x5b, 0xb6, 0x1d, 0x09, 0x87, 0xa0, 0xde,
0x56, 0xfe, 0xfa, 0x90, 0x95, 0xc6, 0x8a, 0xe2, 0xb7, 0x6d, 0x5f, 0x99, 0xc4, 0xd4, 0x63, 0x23, 0x3c, 0xd6, 0x4e,
0xa4, 0x3c, 0x6f, 0xc6, 0x1e, 0xc2, 0x8f, 0xfc, 0x91, 0x85, 0xf7, 0xf0, 0xcb, 0x5b, 0xd6, 0xf9, 0x2c, 0x49, 0xc1,
0xac, 0x9a, 0x72, 0x3e, 0x0f, 0xb6, 0xb6, 0xce, 0xce, 0xce, 0xfc, 0xb3, 0x6d, 0x3f, 0xcb, 0x4f, 0xb6, 0xba, 0xed,
0x76, 0x1b, 0x3f, 0x98, 0x64, 0x5b, 0xa7, 0x31, 0x3b, 0x7b, 0x0c, 0xee, 0x87, 0xfd, 0xd0, 0x7a, 0x64, 0x3d, 0xdc,
0xb6, 0x76, 0x1e, 0xd8, 0x16, 0x29, 0x00, 0x28, 0xd9, 0xb6, 0x2d, 0xa1, 0x00, 0x42, 0x1b, 0x8a, 0xfb, 0xbb, 0x27,
0xca, 0x86, 0xc3, 0x84, 0x74, 0x61, 0x21, 0x81, 0xff, 0x96, 0x7d, 0x62, 0xf5, 0xad, 0x2e, 0xca, 0x5a, 0x52, 0x8d,
0xa8, 0x57, 0xdc, 0xef, 0xa3, 0x68, 0x1e, 0x10, 0x1b, 0x99, 0x85, 0x18, 0x26, 0x13, 0xa5, 0x34, 0x05, 0xda, 0xa5,
0xc7, 0xf0, 0x04, 0x6e, 0xc1, 0xd4, 0x82, 0xe7, 0x57, 0xdd, 0x87, 0xa0, 0xe3, 0x4e, 0x5b, 0xf7, 0x47, 0xed, 0x56,
0xc7, 0xea, 0xb4, 0xba, 0xfe, 0x43, 0xab, 0x2b, 0xfe, 0x07, 0x19, 0xb9, 0x6d, 0x75, 0xe0, 0x69, 0xdb, 0x82, 0xf7,
0xd3, 0xfb, 0x22, 0x1d, 0x22, 0xb2, 0xb7, 0xfa, 0xbb, 0xf8, 0xfb, 0x83, 0x00, 0xa9, 0xaf, 0x6c, 0xf1, 0x1b, 0xcf,
0xec, 0x2f, 0xcc, 0xd2, 0xce, 0xa3, 0xb5, 0xc5, 0xdd, 0x87, 0x6b, 0x8b, 0xb7, 0x1f, 0xac, 0x2d, 0xbe, 0xbf, 0x53,
0x2f, 0xde, 0x3a, 0x11, 0x55, 0x5a, 0x2e, 0x84, 0xf6, 0x2c, 0x02, 0x46, 0x39, 0x77, 0x3a, 0x00, 0x67, 0xdb, 0x6a,
0xe1, 0x8f, 0x87, 0x5d, 0x57, 0xf7, 0x3a, 0xc6, 0x5e, 0x1a, 0xcb, 0x87, 0x8f, 0x00, 0xcb, 0xe7, 0xdd, 0x07, 0x23,
0x6c, 0x47, 0x88, 0xc2, 0xbf, 0xd3, 0xed, 0x47, 0x23, 0xd0, 0x08, 0x16, 0xfe, 0x83, 0x3f, 0xd3, 0x9d, 0xee, 0x48,
0xbc, 0xb4, 0xb1, 0xfe, 0x43, 0xe7, 0x61, 0x01, 0x4d, 0xf1, 0xcf, 0x6f, 0xda, 0x84, 0x46, 0x03, 0xde, 0x1c, 0xf7,
0x3e, 0xd0, 0xe8, 0xd1, 0xb4, 0xeb, 0x7f, 0x75, 0xfa, 0xd0, 0x7f, 0x34, 0xed, 0x3c, 0xfc, 0x20, 0xde, 0x12, 0xa0,
0xe0, 0x57, 0xf8, 0xef, 0xc3, 0x76, 0x7b, 0xda, 0xea, 0xf8, 0x8f, 0x4e, 0xb7, 0xfd, 0xed, 0xa4, 0xf5, 0xc0, 0x7f,
0x84, 0xff, 0xaa, 0xe1, 0xa6, 0xd9, 0x8c, 0xd9, 0x16, 0xae, 0x77, 0xc3, 0xef, 0x35, 0xe7, 0xe8, 0xde, 0xb7, 0x76,
0xee, 0x3f, 0x7f, 0x04, 0x6b, 0x34, 0xed, 0x74, 0xe1, 0xff, 0xab, 0x1e, 0x3f, 0x20, 0xe1, 0xe5, 0xc0, 0x11, 0xc3,
0x54, 0x52, 0x45, 0x38, 0xfa, 0x78, 0xd7, 0x3d, 0xef, 0x87, 0xab, 0x02, 0x20, 0x7f, 0xbe, 0x39, 0x00, 0xf2, 0x97,
0x5b, 0x06, 0xb9, 0xff, 0xfc, 0x07, 0x47, 0x40, 0x7e, 0x68, 0x06, 0xb9, 0xf7, 0xd8, 0x4a, 0xa0, 0xa3, 0xe9, 0xac,
0x3d, 0x67, 0xce, 0xe1, 0x8f, 0x6c, 0x88, 0x69, 0xd3, 0xd0, 0xfa, 0x2f, 0xb5, 0x78, 0x50, 0x86, 0x1b, 0x79, 0x8f,
0x89, 0x9d, 0xcc, 0xf8, 0x15, 0x04, 0xe1, 0xfc, 0x46, 0x82, 0xbc, 0xb8, 0x1d, 0x3d, 0x38, 0xff, 0x63, 0xe9, 0x41,
0x5f, 0xee, 0x57, 0xf4, 0xd0, 0x49, 0xe1, 0x85, 0xf1, 0xfb, 0x57, 0x16, 0x79, 0xa2, 0xaf, 0xa8, 0x65, 0x23, 0xca,
0xf4, 0xf4, 0x91, 0x97, 0xe8, 0x9e, 0x20, 0x54, 0x6e, 0x86, 0xf0, 0x9f, 0xf1, 0x09, 0xb0, 0x2d, 0xfc, 0xd4, 0x9c,
0x1d, 0xc0, 0x4f, 0xaa, 0xb5, 0x19, 0xc6, 0x0a, 0x0a, 0xbb, 0xac, 0x85, 0xf3, 0x29, 0x1c, 0x41, 0x51, 0x84, 0x7d,
0x7a, 0x77, 0x70, 0x46, 0x91, 0x63, 0xf8, 0xee, 0x73, 0x4e, 0x1d, 0x44, 0xb6, 0x72, 0x68, 0xcb, 0xc0, 0xce, 0x1c,
0xbf, 0x79, 0xab, 0x5e, 0x4e, 0xc5, 0x8d, 0x98, 0x90, 0xae, 0xb3, 0xed, 0xe8, 0xa0, 0x10, 0x26, 0x90, 0x87, 0xcb,
0x8c, 0x47, 0xf8, 0x4b, 0x95, 0x78, 0xcc, 0x13, 0xbc, 0x6c, 0x2b, 0x2f, 0xa6, 0x61, 0xbe, 0xf3, 0x77, 0x98, 0xab,
0xad, 0x30, 0x9e, 0x61, 0x20, 0xe2, 0x15, 0x1b, 0xc7, 0x91, 0x63, 0x3b, 0x73, 0x90, 0x04, 0x30, 0x66, 0xab, 0x96,
0x28, 0x4d, 0x39, 0xd2, 0xae, 0xad, 0x7e, 0x8f, 0x4f, 0x8e, 0xdf, 0x45, 0x0b, 0x0f, 0x65, 0xe0, 0xd2, 0x96, 0x9e,
0x05, 0x8c, 0xaf, 0x4a, 0x72, 0x54, 0x22, 0x95, 0x35, 0x62, 0x0b, 0x73, 0xa4, 0xe3, 0x77, 0xc1, 0x3d, 0xa1, 0x62,
0xfc, 0xce, 0xd4, 0x0f, 0x4e, 0x6b, 0x1b, 0xcc, 0x25, 0xa3, 0x9b, 0x1e, 0x68, 0xb8, 0x12, 0x9e, 0x12, 0x41, 0x87,
0x46, 0xa9, 0xa7, 0x7f, 0xb1, 0x59, 0x15, 0x86, 0x2a, 0x1e, 0x5f, 0x1c, 0xc8, 0xbb, 0xe4, 0x36, 0x46, 0x84, 0xe9,
0x24, 0xa0, 0xfa, 0x8e, 0xd5, 0x26, 0xa8, 0x21, 0xfa, 0xed, 0x92, 0x74, 0x9e, 0x8e, 0x9a, 0x06, 0x53, 0xb9, 0x2d,
0xe9, 0x91, 0x84, 0xb6, 0xd0, 0x33, 0x63, 0x76, 0x1a, 0x8f, 0xa4, 0xba, 0xf0, 0x2c, 0x79, 0x05, 0xa5, 0x45, 0x58,
0x84, 0x1d, 0x4f, 0xf8, 0xe4, 0xf0, 0x82, 0xda, 0x32, 0x4c, 0x33, 0xbb, 0x7f, 0xaf, 0xa7, 0x21, 0xa9, 0x67, 0xc1,
0xdb, 0xf8, 0xab, 0x2e, 0x0f, 0xc1, 0x07, 0xfc, 0xfb, 0xf0, 0x1e, 0xfe, 0xb2, 0xcb, 0x7b, 0x43, 0xdb, 0xf5, 0x49,
0xd8, 0xde, 0xab, 0x7e, 0xe3, 0x25, 0x4a, 0x9a, 0x4d, 0xd0, 0x8b, 0xde, 0x6d, 0x15, 0xa4, 0x54, 0x86, 0xdd, 0x9d,
0x4a, 0x19, 0xc2, 0xb3, 0x21, 0xfd, 0x40, 0x30, 0x77, 0xfd, 0x1d, 0x43, 0xc4, 0x9e, 0xb6, 0xf0, 0x67, 0x4d, 0xc8,
0xde, 0x87, 0x06, 0x4a, 0xca, 0xbe, 0x84, 0xe6, 0xdb, 0x42, 0xa0, 0x43, 0xbf, 0x1f, 0x49, 0x04, 0x0a, 0xf1, 0x57,
0xe7, 0x98, 0x35, 0x87, 0x53, 0x22, 0xf7, 0xc0, 0xf6, 0x8c, 0x38, 0x96, 0x60, 0x57, 0x19, 0xa5, 0x79, 0x0a, 0x7d,
0x1d, 0xfd, 0x79, 0xe8, 0x75, 0x75, 0x5e, 0x6d, 0xd3, 0xac, 0x99, 0x02, 0x19, 0xbe, 0x71, 0x00, 0x46, 0x57, 0x22,
0xc4, 0x67, 0xd2, 0x84, 0x78, 0xa8, 0x3e, 0x24, 0xb7, 0x26, 0xab, 0xd7, 0xdc, 0x14, 0xac, 0x62, 0x1a, 0xda, 0x17,
0x98, 0x8a, 0x83, 0x3f, 0xab, 0x62, 0xf5, 0x20, 0x19, 0xca, 0x4f, 0x22, 0xfc, 0x2d, 0x2f, 0xf4, 0xa3, 0xac, 0x36,
0x20, 0xa7, 0xef, 0x60, 0x12, 0xa4, 0x2f, 0xc6, 0x65, 0x13, 0x09, 0xb0, 0x43, 0xf0, 0x97, 0x06, 0x56, 0x57, 0x30,
0xe4, 0xdd, 0x4a, 0xcc, 0x55, 0x18, 0xc7, 0x39, 0x5d, 0xd9, 0x55, 0xf8, 0xd7, 0x22, 0xa5, 0x15, 0xa9, 0x69, 0x57,
0xb2, 0x62, 0x60, 0x63, 0x11, 0x88, 0x1a, 0x91, 0xe4, 0x66, 0x7e, 0x08, 0xda, 0xbc, 0x53, 0x39, 0x16, 0xf9, 0x6d,
0xf8, 0xa1, 0x7c, 0x5b, 0x10, 0xd9, 0x06, 0xf1, 0x78, 0x25, 0x4e, 0x64, 0x34, 0xe1, 0x55, 0xc4, 0xea, 0x37, 0x1f,
0x99, 0x1b, 0xde, 0x36, 0x57, 0x4b, 0x8f, 0x4b, 0xeb, 0xe0, 0xca, 0xb8, 0xe0, 0x31, 0x8b, 0xb8, 0x1f, 0xa5, 0x94,
0xf3, 0xe4, 0x18, 0x62, 0xc1, 0xeb, 0xb0, 0x6d, 0xb7, 0x04, 0xc9, 0x63, 0xfc, 0x6a, 0x28, 0x41, 0x7a, 0x1f, 0x0a,
0xab, 0x44, 0xb0, 0xdd, 0x69, 0xb7, 0xff, 0xe6, 0x60, 0xcf, 0x12, 0xbb, 0x79, 0x77, 0x0b, 0x5e, 0x77, 0xc9, 0xcd,
0x16, 0x79, 0x1f, 0xa1, 0xc8, 0xfb, 0xb0, 0x44, 0xa2, 0x58, 0x68, 0x6f, 0x09, 0x34, 0x6d, 0x8b, 0xa5, 0x23, 0x11,
0x1b, 0x9c, 0x81, 0x1b, 0x12, 0xe3, 0xc7, 0xc2, 0xb6, 0xb0, 0x5b, 0x0b, 0x57, 0xda, 0x56, 0x99, 0x33, 0xca, 0xf0,
0xe0, 0xa9, 0x8a, 0x24, 0x82, 0xb9, 0xc0, 0x54, 0x12, 0x8d, 0x1c, 0x4a, 0xe7, 0xba, 0xae, 0xb6, 0x2e, 0x16, 0xc7,
0x33, 0x90, 0x43, 0x2a, 0xf1, 0xe5, 0xbd, 0xec, 0xb0, 0x4b, 0x53, 0x61, 0xb2, 0xed, 0x6a, 0xa4, 0x73, 0xda, 0xe9,
0xef, 0x46, 0xd2, 0x8e, 0xc2, 0xbd, 0x5b, 0xc0, 0xe6, 0x05, 0xf5, 0x89, 0xc6, 0x8a, 0x1f, 0x67, 0x5b, 0x67, 0xec,
0xb8, 0x15, 0xcd, 0xe3, 0x2a, 0xac, 0x88, 0x5a, 0xb5, 0xbf, 0xab, 0x14, 0xac, 0x4c, 0xdf, 0x94, 0x8f, 0x91, 0x91,
0x1d, 0x82, 0x84, 0x23, 0x06, 0x2d, 0x65, 0xcc, 0x92, 0x8c, 0x51, 0x20, 0x3e, 0xc0, 0x4a, 0xfc, 0xab, 0x62, 0x9b,
0x52, 0x13, 0x94, 0x76, 0xff, 0xaf, 0xff, 0xeb, 0x7f, 0xcb, 0x70, 0x25, 0x90, 0x15, 0xc0, 0xc2, 0xf4, 0x9a, 0xea,
0xe4, 0x92, 0x9d, 0x83, 0x83, 0x1b, 0x8f, 0x5b, 0xd3, 0x28, 0x99, 0x00, 0x04, 0x05, 0x13, 0xda, 0x50, 0xd6, 0x03,
0x17, 0x48, 0xb0, 0xcc, 0x43, 0x7f, 0x09, 0x5e, 0xbd, 0x08, 0x57, 0xec, 0x77, 0xe5, 0xc3, 0xaa, 0x3c, 0x64, 0x62,
0x68, 0x23, 0x3b, 0xd6, 0xe0, 0xb9, 0x5a, 0x86, 0xac, 0xfa, 0xc5, 0x4b, 0x52, 0x78, 0xb0, 0x5a, 0x7a, 0x2c, 0xb4,
0xd4, 0x07, 0x2c, 0xff, 0xf6, 0xcf, 0xff, 0xf9, 0xbf, 0xab, 0x57, 0x3c, 0x37, 0xf9, 0xeb, 0x3f, 0xfd, 0xc3, 0xff,
0xfd, 0x3f, 0xff, 0x05, 0xb3, 0x8f, 0xe5, 0xd9, 0x0a, 0x6d, 0x25, 0xab, 0x3a, 0x58, 0x11, 0x7b, 0xca, 0xaa, 0x1c,
0x99, 0x7a, 0x1a, 0xed, 0x3e, 0x4d, 0x48, 0xbc, 0x29, 0xa1, 0x23, 0xbe, 0xa6, 0xb4, 0x6b, 0xa2, 0xda, 0x35, 0xe4,
0x83, 0xa5, 0xb4, 0x28, 0x5d, 0xc0, 0xde, 0x69, 0xdb, 0xd5, 0xf2, 0xf6, 0x8d, 0xbe, 0x5b, 0xb8, 0x30, 0xb7, 0xca,
0xec, 0xf1, 0xf5, 0xb2, 0x2d, 0x55, 0x78, 0x0c, 0x4b, 0xca, 0xaa, 0xdc, 0xc2, 0xb8, 0xf5, 0x12, 0x5f, 0x83, 0xae,
0x51, 0x4c, 0xab, 0x5c, 0xeb, 0xd3, 0xfb, 0x65, 0x01, 0x88, 0x4e, 0x70, 0x69, 0x44, 0x10, 0x8e, 0xce, 0x64, 0x5b,
0x68, 0xc0, 0x24, 0x17, 0x25, 0x8d, 0x22, 0xbc, 0xa4, 0xfb, 0x8f, 0xfe, 0xae, 0xfc, 0xd3, 0x0c, 0xad, 0x02, 0xcb,
0x99, 0x45, 0xe7, 0xd2, 0x77, 0x7a, 0xd0, 0x6e, 0xcf, 0xcf, 0xdd, 0x65, 0x35, 0x83, 0x77, 0xd5, 0x64, 0x14, 0xb8,
0x33, 0x07, 0xa4, 0xc3, 0x5c, 0x1d, 0x23, 0x04, 0x77, 0xa1, 0x8d, 0x21, 0xa5, 0xb2, 0xfc, 0x72, 0x49, 0x61, 0xaa,
0xf8, 0x37, 0x3c, 0x74, 0x95, 0x11, 0x3d, 0x28, 0x31, 0xb0, 0x58, 0x1a, 0xbd, 0xba, 0xa2, 0xd7, 0xb4, 0xb3, 0x9a,
0xf3, 0x62, 0x1e, 0x1a, 0x9b, 0xc7, 0xbd, 0xf7, 0xf1, 0x00, 0x77, 0xda, 0xf1, 0xa6, 0xdd, 0xa5, 0x1e, 0x9e, 0xf3,
0x6c, 0x66, 0x9e, 0x12, 0xb3, 0x88, 0x8d, 0xd8, 0x44, 0x45, 0x42, 0x65, 0xbd, 0x38, 0x01, 0x2e, 0xbf, 0xc0, 0xed,
0x06, 0xb4, 0xcd, 0x22, 0x1e, 0x10, 0xd3, 0xf6, 0xcc, 0x73, 0xe4, 0x08, 0x4f, 0xe8, 0xb3, 0xa5, 0x31, 0x57, 0x4f,
0x34, 0xc5, 0x78, 0x63, 0x3d, 0x9f, 0xa8, 0xf4, 0xa9, 0xbb, 0x39, 0x94, 0x08, 0x57, 0xbc, 0x90, 0xc7, 0xb3, 0xef,
0x6a, 0x7e, 0xbe, 0x14, 0xc5, 0xe0, 0x5a, 0xaf, 0xad, 0x17, 0x6a, 0x51, 0xd4, 0xbe, 0x00, 0x6b, 0x87, 0xc0, 0xb4,
0x9b, 0xad, 0xa8, 0x10, 0x5b, 0xbd, 0x0b, 0x5f, 0x68, 0x9b, 0x3e, 0x9a, 0xcf, 0xa9, 0xa1, 0x0b, 0xdc, 0x48, 0xb6,
0x39, 0x4a, 0x0a, 0x4a, 0x3d, 0x10, 0x27, 0xfd, 0xb2, 0x8d, 0x64, 0x5b, 0xf1, 0x24, 0x73, 0x00, 0xe8, 0xf7, 0x78,
0xff, 0x3f, 0x32, 0x18, 0x26, 0x95, 0xdd, 0x7b, 0x00, 0x00};
0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc5, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36,
0x43, 0x41, 0xe1, 0x78, 0xf3, 0x80, 0x05, 0xd3, 0x7e, 0xd8, 0x1e, 0x38, 0xb9, 0x50, 0x1d, 0x09, 0x9e, 0x5b, 0x0a,
0x09, 0xde, 0xf6, 0xa6, 0x20, 0xd0, 0x91, 0x73, 0x37, 0xec, 0x4d, 0x55, 0x08, 0x45, 0x1f, 0x37, 0xc7, 0x6e, 0x10,
0xc3, 0x0f, 0xa7, 0x85, 0x4c, 0x33, 0xd5, 0x7d, 0xb5, 0x66, 0x76, 0x83, 0xb1, 0xb2, 0xc8, 0x93, 0x30, 0xdb, 0x74,
0x30, 0x42, 0x0b, 0x92, 0x76, 0x77, 0x00, 0x30, 0x6c, 0x3a, 0x8a, 0xd3, 0xb6, 0x14, 0xab, 0x29, 0xfb, 0xfc, 0x50,
0x2f, 0xc7, 0x94, 0x0d, 0xa6, 0xcc, 0xaf, 0xb4, 0x0f, 0x80, 0x15, 0x24, 0x5e, 0x3e, 0x54, 0x67, 0x5e, 0xcf, 0x6b,
0xe7, 0x5b, 0x4b, 0x25, 0x8a, 0x98, 0x67, 0x48, 0x28, 0x5e, 0x6a, 0x37, 0x4c, 0x98, 0xdb, 0x73, 0x24, 0x86, 0x66,
0xf9, 0xb0, 0x0d, 0x4c, 0xaf, 0x02, 0xec, 0xa9, 0xb9, 0x2d, 0x92, 0xb0, 0x6a, 0xee, 0x1d, 0x02, 0x6b, 0x0f, 0x81,
0x87, 0x68, 0x1b, 0xf5, 0x54, 0x34, 0x9f, 0x25, 0xe1, 0xf3, 0xc6, 0x71, 0x71, 0x84, 0x27, 0x42, 0xfb, 0xfe, 0x68,
0x91, 0x83, 0x3c, 0xe0, 0xaf, 0xc1, 0x32, 0x08, 0x65, 0x53, 0x74, 0xf4, 0xf0, 0x08, 0xd8, 0x23, 0xc4, 0x1b, 0x61,
0x73, 0xa3, 0x1a, 0x2d, 0x4a, 0x32, 0x5e, 0xe8, 0x60, 0xb8, 0xc7, 0xa5, 0x6b, 0x8f, 0x82, 0x41, 0x9e, 0x18, 0x3b,
0x78, 0xe6, 0xef, 0x8f, 0xb0, 0x1a, 0x27, 0x28, 0xdc, 0x92, 0x76, 0x5b, 0x25, 0xfe, 0xf6, 0xfd, 0x14, 0x24, 0x38,
0xd6, 0x81, 0x9f, 0x75, 0xf7, 0x6e, 0x22, 0x91, 0xda, 0x4d, 0x7b, 0x74, 0x12, 0x81, 0xf1, 0xe0, 0xdc, 0x4f, 0xa1,
0x1a, 0x49, 0x44, 0x45, 0x39, 0x5a, 0xa0, 0xe6, 0xa9, 0x5a, 0x05, 0xdf, 0xa1, 0x19, 0x81, 0xe7, 0x18, 0xb6, 0x26,
0x3f, 0x55, 0x37, 0x16, 0xb1, 0x7c, 0xd7, 0xa5, 0xa3, 0x2d, 0x3c, 0x80, 0x14, 0x8c, 0x26, 0x18, 0xc6, 0xa5, 0xa0,
0x64, 0xc5, 0x7f, 0x1f, 0x8d, 0x58, 0xf9, 0xf4, 0x30, 0xdb, 0xdc, 0x1c, 0x8a, 0x73, 0x0b, 0x62, 0x1c, 0x6e, 0x44,
0x57, 0xe3, 0x0a, 0x80, 0xfa, 0x74, 0x4e, 0x5c, 0x0f, 0x4c, 0x2b, 0xd6, 0x74, 0x29, 0xf6, 0xc9, 0x61, 0x06, 0xa0,
0xe0, 0x96, 0x73, 0xe8, 0x0f, 0xfe, 0x3c, 0x04, 0xf7, 0xd8, 0xff, 0x93, 0xbb, 0xa5, 0x04, 0x4d, 0x4f, 0x9e, 0x29,
0x2e, 0xe9, 0x8c, 0xb5, 0xe3, 0x51, 0x6c, 0x34, 0x28, 0xbc, 0x14, 0x30, 0x00, 0x6d, 0x0e, 0x32, 0xa1, 0xe2, 0x20,
0xe4, 0xa8, 0xc0, 0xf6, 0x71, 0xf3, 0x73, 0xdc, 0xd9, 0x4f, 0xc1, 0xc2, 0x1b, 0xe8, 0xb7, 0x97, 0xf0, 0xf6, 0x67,
0xfd, 0xf6, 0x0b, 0x0b, 0x7e, 0x29, 0x65, 0xe8, 0xbe, 0x36, 0xc5, 0x03, 0x35, 0x45, 0x29, 0x96, 0xc8, 0xa0, 0x21,
0x73, 0xf3, 0x95, 0x98, 0x0d, 0x77, 0x4b, 0x20, 0x86, 0x12, 0x5d, 0xb9, 0xcf, 0xa3, 0x13, 0x24, 0xae, 0x6b, 0x92,
0xc2, 0xc8, 0x25, 0x30, 0x11, 0xae, 0xf8, 0x96, 0x98, 0xb3, 0xdf, 0x06, 0x1b, 0xbc, 0x96, 0x77, 0x80, 0xf6, 0x1d,
0x9b, 0xcd, 0xf9, 0xc5, 0x3e, 0x29, 0xfa, 0x40, 0xa6, 0x0d, 0x88, 0xb3, 0xf3, 0x76, 0x2f, 0xde, 0xe5, 0xbd, 0x18,
0xa4, 0x7a, 0xae, 0x58, 0x0c, 0xf7, 0xaa, 0xf7, 0x16, 0xa3, 0x94, 0x26, 0x33, 0x79, 0x35, 0xf4, 0xba, 0x12, 0xbd,
0xcd, 0x4d, 0x40, 0xb0, 0x67, 0x74, 0xe5, 0xa2, 0x6b, 0x59, 0x0a, 0x9a, 0x00, 0x44, 0x8f, 0xea, 0x2c, 0x47, 0x1c,
0x87, 0xd9, 0x6c, 0x50, 0x3c, 0x62, 0xee, 0xda, 0x51, 0x71, 0x4c, 0xec, 0x2e, 0x13, 0x76, 0x00, 0x33, 0xe2, 0xf2,
0x56, 0x47, 0x44, 0x87, 0x45, 0x7f, 0x1d, 0xdf, 0xfe, 0xe8, 0xb1, 0xcd, 0x8e, 0x0b, 0x1a, 0xa4, 0x36, 0xd6, 0xc3,
0x6a, 0x2c, 0xa8, 0x0f, 0x3f, 0x6a, 0x2a, 0x95, 0xc5, 0xe6, 0x66, 0x59, 0x3f, 0xaa, 0x55, 0x3b, 0xb8, 0x76, 0x9a,
0x72, 0xde, 0xcc, 0x06, 0xe1, 0x40, 0xc4, 0x04, 0x0a, 0xb4, 0xb4, 0xb2, 0x62, 0x80, 0x21, 0x65, 0x39, 0xca, 0xa7,
0x90, 0x79, 0x71, 0x59, 0xea, 0xd4, 0x17, 0x19, 0x8f, 0x0c, 0xf1, 0xd4, 0x93, 0x8c, 0x15, 0x50, 0xb0, 0x5e, 0xea,
0x25, 0xb4, 0x44, 0x80, 0xf9, 0x33, 0x95, 0x43, 0x23, 0x2c, 0x90, 0x28, 0x34, 0xcc, 0x12, 0x65, 0x7c, 0x16, 0x61,
0x0c, 0xda, 0xfe, 0x49, 0x2d, 0xf6, 0x55, 0x28, 0xa3, 0xa3, 0x38, 0xcc, 0x87, 0x01, 0xd5, 0x2f, 0xa4, 0x04, 0x9b,
0x86, 0xef, 0x81, 0x8d, 0x2a, 0xc7, 0x93, 0x04, 0xe1, 0xd3, 0x38, 0x67, 0xe4, 0x29, 0x6c, 0x48, 0x98, 0xa5, 0x69,
0x1b, 0xa9, 0x76, 0x91, 0x19, 0x84, 0x72, 0x51, 0xf0, 0x1a, 0x67, 0x17, 0x59, 0xb8, 0xd2, 0x1a, 0xcc, 0x8f, 0x37,
0x26, 0x40, 0xd9, 0xe5, 0x65, 0x26, 0x7c, 0xdc, 0x88, 0xec, 0x0d, 0x5d, 0x31, 0x1d, 0x28, 0xa4, 0x02, 0x27, 0x22,
0x8b, 0x87, 0xce, 0x50, 0x68, 0x84, 0x03, 0x3a, 0x45, 0xce, 0x5d, 0x63, 0xd3, 0xe7, 0x03, 0xed, 0x1b, 0xa5, 0xa1,
0x93, 0x80, 0x10, 0x10, 0xb8, 0x1b, 0xd6, 0x54, 0x3a, 0x48, 0x83, 0x84, 0x4a, 0xd1, 0xcf, 0x01, 0xfc, 0xc3, 0x48,
0x52, 0x00, 0xec, 0x87, 0x6a, 0xa4, 0x88, 0xb2, 0x2c, 0x70, 0x01, 0x68, 0xae, 0x7d, 0x5c, 0x09, 0x5f, 0x18, 0xa8,
0x30, 0x3d, 0xcd, 0xca, 0x4b, 0xa1, 0x44, 0x1e, 0xaf, 0x49, 0x59, 0x23, 0x99, 0x7c, 0x8a, 0x0e, 0x9f, 0xf2, 0xae,
0x5f, 0x4b, 0x3c, 0x74, 0xc1, 0x53, 0x58, 0x56, 0xf5, 0xfc, 0x2a, 0xe4, 0xe4, 0x5c, 0x83, 0xae, 0x90, 0x42, 0x7f,
0xc5, 0x49, 0xde, 0x7b, 0xe5, 0x57, 0xb5, 0xd4, 0x18, 0xca, 0xde, 0xaf, 0x6b, 0x86, 0xe5, 0xe5, 0xbc, 0x0a, 0x53,
0x10, 0x70, 0x4b, 0x96, 0x04, 0x4b, 0xa9, 0x21, 0xc0, 0xc2, 0xf6, 0x48, 0x2b, 0x05, 0x79, 0xa9, 0xc3, 0x3b, 0x4f,
0xc1, 0x0a, 0x30, 0x0e, 0xb5, 0x54, 0x32, 0x8d, 0x24, 0xbe, 0x54, 0xa2, 0xc0, 0x94, 0xfb, 0x23, 0xf0, 0x53, 0x9b,
0x27, 0x5d, 0xe7, 0xae, 0x1f, 0xcf, 0x30, 0xb5, 0x87, 0x40, 0x8f, 0xbd, 0x3b, 0x60, 0x4a, 0xd4, 0x75, 0x58, 0x41,
0x1c, 0x9a, 0xd5, 0x34, 0x0b, 0x98, 0x31, 0x6d, 0xd0, 0x92, 0x6d, 0xb0, 0xe5, 0x72, 0xb0, 0x8f, 0xc4, 0xf6, 0xac,
0x56, 0x40, 0xe8, 0x1a, 0x34, 0x30, 0xe4, 0x2e, 0x15, 0x5a, 0x98, 0xf7, 0xba, 0x54, 0x84, 0xfb, 0x73, 0xc0, 0xa5,
0x15, 0x9c, 0x79, 0x19, 0x0d, 0xbc, 0x1f, 0x1f, 0x27, 0x98, 0xf8, 0x82, 0x58, 0x81, 0x1d, 0x1c, 0x74, 0x9a, 0x4d,
0x81, 0x53, 0x71, 0x91, 0x32, 0x58, 0x56, 0x94, 0xda, 0xf0, 0x43, 0x8a, 0x6c, 0xdd, 0xe5, 0x81, 0xee, 0x42, 0x2c,
0x80, 0x9d, 0x7e, 0xc3, 0xc8, 0xb7, 0xac, 0x97, 0x01, 0x83, 0x53, 0xad, 0x71, 0x10, 0xf8, 0xcd, 0xcd, 0x64, 0x58,
0xa6, 0xc4, 0x76, 0x4d, 0x56, 0x17, 0x90, 0xc3, 0x50, 0x4d, 0xdc, 0x41, 0x58, 0x2a, 0x7b, 0xbc, 0x28, 0x67, 0xb8,
0x5c, 0xca, 0x42, 0x6e, 0x9e, 0x57, 0xd3, 0x7c, 0x6e, 0xa5, 0xd9, 0x74, 0xbc, 0x15, 0x5f, 0x14, 0xfc, 0x03, 0x27,
0x96, 0x56, 0x3d, 0xa5, 0x56, 0x78, 0x94, 0xb9, 0x25, 0xeb, 0x94, 0xd4, 0xea, 0xba, 0x81, 0x6a, 0x84, 0xa7, 0x69,
0xd8, 0x08, 0x84, 0x98, 0xe0, 0xe2, 0xd7, 0x4d, 0x26, 0xa6, 0xbd, 0x25, 0xa4, 0x8e, 0xb0, 0x7b, 0x28, 0x27, 0xb8,
0xab, 0x79, 0xf6, 0x79, 0x38, 0xbf, 0x9a, 0xb9, 0xf7, 0x0c, 0xe6, 0x7e, 0x1c, 0x72, 0x83, 0xd1, 0x63, 0x99, 0xf0,
0x23, 0x63, 0x1f, 0xb9, 0xaa, 0x7a, 0x72, 0x12, 0x56, 0x22, 0x4b, 0x3c, 0x19, 0x47, 0x1d, 0xc6, 0xa9, 0x68, 0x4d,
0x90, 0x5d, 0x5e, 0x16, 0xe6, 0x5e, 0xa0, 0xa0, 0xa9, 0xc7, 0xeb, 0x71, 0xda, 0x8a, 0x9d, 0x8d, 0x48, 0xe4, 0xde,
0xab, 0x5a, 0x24, 0xb2, 0xe2, 0x73, 0x1c, 0xe9, 0x8a, 0x83, 0xdc, 0x27, 0x27, 0xab, 0x9b, 0x54, 0xe8, 0x16, 0x8d,
0xb6, 0xb1, 0x47, 0xf5, 0x81, 0xa4, 0x9e, 0x51, 0x81, 0x55, 0x8d, 0x7d, 0xf7, 0x6e, 0x47, 0xa4, 0x5b, 0x2a, 0xc5,
0x06, 0x4b, 0x0b, 0xa3, 0x19, 0xa3, 0x60, 0x50, 0x52, 0x64, 0xa0, 0x46, 0xf9, 0x15, 0x82, 0x61, 0x8f, 0x1a, 0x80,
0xe2, 0x5c, 0x5f, 0xfd, 0xb8, 0x94, 0x6c, 0x21, 0x20, 0x71, 0x97, 0x0c, 0xc4, 0x9a, 0x60, 0x66, 0xe4, 0x93, 0xf7,
0xc0, 0x79, 0x03, 0x86, 0x0e, 0x01, 0xf8, 0x05, 0x62, 0xd3, 0x83, 0x89, 0x6d, 0x13, 0x51, 0xf4, 0xd9, 0xc0, 0x73,
0x00, 0x76, 0x5e, 0x85, 0x46, 0xdf, 0x55, 0x29, 0x60, 0xc8, 0x06, 0x6e, 0xc0, 0xaa, 0xb0, 0xdc, 0xde, 0x73, 0x70,
0x1b, 0xe0, 0xf5, 0x99, 0x6c, 0xbe, 0x81, 0x79, 0x82, 0xd5, 0xd9, 0x85, 0x5f, 0x59, 0xd6, 0xe2, 0xdc, 0xe9, 0xa0,
0x51, 0xaf, 0x28, 0x21, 0x6a, 0xf7, 0xb1, 0xf6, 0x39, 0x46, 0x58, 0xc4, 0xfb, 0x2b, 0x7c, 0xd7, 0xe3, 0x96, 0x7b,
0x1a, 0x2d, 0xc2, 0x74, 0x95, 0x34, 0x06, 0x25, 0xeb, 0x7e, 0x32, 0xe2, 0x5e, 0xee, 0x8b, 0x58, 0x70, 0x85, 0x23,
0xab, 0x42, 0x8a, 0x0d, 0x24, 0xe9, 0x69, 0x8f, 0x0e, 0xd8, 0x37, 0x9a, 0xbd, 0x80, 0x32, 0xef, 0x2b, 0x52, 0x49,
0x48, 0x69, 0x76, 0x43, 0x24, 0x09, 0x6b, 0x45, 0x9e, 0x3a, 0xef, 0x3b, 0xda, 0xe7, 0x56, 0x12, 0xc1, 0x08, 0x4e,
0xc2, 0x74, 0xac, 0x3c, 0x68, 0x0a, 0x70, 0x15, 0x1d, 0x31, 0x7d, 0x13, 0x90, 0xdf, 0x0c, 0xe4, 0xf6, 0x4a, 0x72,
0x6d, 0xae, 0x61, 0x78, 0x82, 0x04, 0xab, 0x22, 0x11, 0x78, 0x44, 0x8d, 0x09, 0xc9, 0xeb, 0x3c, 0x0f, 0x30, 0xe1,
0x6b, 0x7b, 0x13, 0x00, 0xca, 0xc9, 0x55, 0x71, 0x56, 0x02, 0xdd, 0x80, 0xe5, 0xfa, 0x38, 0x35, 0x2a, 0x12, 0x17,
0x37, 0xa6, 0xab, 0x5b, 0xfa, 0x33, 0xb4, 0x9c, 0xc9, 0x10, 0xd3, 0x41, 0x10, 0x90, 0xa9, 0x8f, 0x99, 0x23, 0x64,
0xae, 0xb0, 0x3e, 0xe7, 0x4e, 0x6d, 0xea, 0x1e, 0xa3, 0x6e, 0x9e, 0xa4, 0x16, 0xaf, 0xd3, 0xa6, 0x94, 0x88, 0x49,
0x89, 0x39, 0x13, 0xa9, 0xd8, 0x4c, 0x89, 0x3b, 0xb7, 0xbe, 0xd1, 0x42, 0xda, 0x68, 0x33, 0x91, 0x83, 0xcd, 0x2a,
0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x91, 0xb1, 0x96, 0x63, 0xe6, 0x98, 0x08, 0x56, 0x2f, 0xa6, 0x22, 0x7f,
0xe7, 0xe8, 0x34, 0x7b, 0x83, 0x1e, 0xa4, 0xde, 0x40, 0x62, 0xd6, 0xc4, 0x77, 0x21, 0x0d, 0x75, 0x84, 0x40, 0x65,
0x54, 0xcb, 0x74, 0x9c, 0x58, 0x85, 0x6f, 0x04, 0x5f, 0xbd, 0xd5, 0xc7, 0xf9, 0xc6, 0x73, 0x63, 0x35, 0x82, 0x18,
0xbc, 0x85, 0x7c, 0xe8, 0x49, 0x11, 0x0e, 0x84, 0xcb, 0x37, 0x37, 0x7b, 0xf9, 0x2e, 0xaf, 0x42, 0x24, 0x15, 0x8c,
0x31, 0x66, 0x14, 0xe3, 0x9e, 0xa8, 0xa9, 0xc5, 0x1c, 0x06, 0x96, 0xad, 0xc3, 0x1c, 0x0f, 0x00, 0xa0, 0xa5, 0x29,
0xbd, 0x6a, 0x2a, 0x54, 0x9e, 0xe7, 0x12, 0x3e, 0xd5, 0x21, 0xaa, 0x6a, 0xfc, 0x76, 0x7d, 0x06, 0x0a, 0xc1, 0x7d,
0xa7, 0xe3, 0xe1, 0x21, 0x04, 0xac, 0xa2, 0x90, 0x05, 0x7a, 0x83, 0xf6, 0xaa, 0x44, 0x28, 0x66, 0x4e, 0xd6, 0x63,
0x86, 0x93, 0x0a, 0xb6, 0x50, 0x09, 0x4b, 0xa5, 0x05, 0x7e, 0xb5, 0x11, 0x9a, 0xa7, 0x8c, 0x7b, 0xaf, 0x2a, 0x9c,
0x41, 0x7f, 0x30, 0x6f, 0x95, 0x51, 0xdf, 0xae, 0x9c, 0xc8, 0x54, 0x60, 0xe2, 0x66, 0x96, 0xda, 0xef, 0x97, 0x75,
0xda, 0xcf, 0x2b, 0xe4, 0x3e, 0x27, 0xcd, 0xd7, 0xb9, 0x85, 0xe6, 0x93, 0xe1, 0x7e, 0xa5, 0xfc, 0xd0, 0xc2, 0xa8,
0x29, 0xbf, 0xbc, 0xae, 0xfc, 0x0a, 0x4f, 0x85, 0xb7, 0xfa, 0x5d, 0x14, 0xba, 0xa8, 0xcf, 0xc1, 0x10, 0xd2, 0x8f,
0xe0, 0x1a, 0x1a, 0x3c, 0x28, 0x92, 0xc5, 0x62, 0xed, 0x82, 0xb8, 0x3e, 0xe6, 0x54, 0x3b, 0x94, 0x31, 0x46, 0x3c,
0x2d, 0x39, 0x48, 0x32, 0x38, 0x18, 0xbf, 0x81, 0x01, 0x31, 0x29, 0x09, 0xe9, 0x10, 0x3a, 0x6b, 0x33, 0x11, 0x95,
0xbb, 0x78, 0xb3, 0x71, 0x59, 0x53, 0x28, 0xc2, 0x4e, 0x30, 0x53, 0x29, 0x15, 0x04, 0xd2, 0xe4, 0xbb, 0xd3, 0xa9,
0x05, 0x43, 0x0b, 0xd7, 0x54, 0x40, 0x5e, 0xdb, 0xf5, 0xa0, 0xc9, 0x7b, 0x8a, 0xa1, 0xaf, 0x53, 0x23, 0x5e, 0x66,
0xf0, 0x35, 0x6c, 0xfe, 0x9a, 0x28, 0xc9, 0x43, 0x26, 0x62, 0xaf, 0xe0, 0x13, 0x21, 0x9b, 0x82, 0x9d, 0x09, 0xf4,
0x43, 0xbb, 0xb2, 0x97, 0xee, 0x16, 0x95, 0x4b, 0x8b, 0xc6, 0x56, 0xa2, 0x66, 0xcd, 0x0f, 0xe3, 0xcd, 0x14, 0xf6,
0xb3, 0x47, 0x09, 0x04, 0xa4, 0xa9, 0x9c, 0xa4, 0x9a, 0xf7, 0x30, 0x1d, 0x02, 0x48, 0xb0, 0xfb, 0x09, 0x2c, 0xf4,
0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, 0x73, 0xd0, 0x9a, 0x73, 0xd2, 0x7c, 0x73, 0xd4, 0xda, 0x9b, 0xca, 0x7a,
0xc6, 0xec, 0x00, 0xdb, 0x76, 0x37, 0x8b, 0xc3, 0x74, 0xb3, 0x33, 0x34, 0x04, 0x17, 0x1e, 0xff, 0x27, 0x25, 0xa6,
0x81, 0xe4, 0x52, 0x37, 0x7e, 0x42, 0x1d, 0x86, 0xff, 0x2d, 0x49, 0x01, 0x0f, 0x6a, 0xab, 0xb1, 0xe2, 0xdc, 0x2b,
0x8e, 0x92, 0xcb, 0xaa, 0xda, 0xd5, 0x12, 0x34, 0x74, 0x23, 0x19, 0x13, 0xc5, 0x3c, 0x27, 0x00, 0x46, 0xb1, 0xf9,
0x53, 0xa6, 0x93, 0xbc, 0x7f, 0x59, 0x9b, 0xda, 0xed, 0xfb, 0x7e, 0x94, 0x9f, 0xd0, 0x91, 0x8a, 0xca, 0xe6, 0x24,
0xe6, 0xdf, 0x16, 0x60, 0x9a, 0x13, 0x1f, 0xea, 0xb9, 0x86, 0xa1, 0x00, 0x5f, 0xd9, 0x50, 0x6a, 0xb6, 0x97, 0xbf,
0x77, 0xb6, 0xfb, 0x92, 0x28, 0x82, 0x05, 0x1a, 0x74, 0xb9, 0x02, 0x5f, 0xc0, 0x32, 0xb8, 0x25, 0xfd, 0xf4, 0xa6,
0xbf, 0x0a, 0x3e, 0x63, 0xff, 0x0b, 0x40, 0xab, 0x02, 0x03, 0xca, 0x9d, 0xa6, 0x61, 0x25, 0xc4, 0x25, 0x2a, 0xcc,
0x2a, 0xce, 0x1f, 0xd7, 0x79, 0xdd, 0xb4, 0x2c, 0x31, 0x28, 0x3f, 0x77, 0x0d, 0x37, 0xbe, 0xd7, 0xc8, 0x1f, 0xdf,
0x7b, 0x0e, 0xba, 0x9d, 0x48, 0x7b, 0xf7, 0x6e, 0x7e, 0x87, 0x2c, 0x34, 0xbc, 0x17, 0x36, 0x87, 0xb6, 0x48, 0x97,
0x5c, 0x3d, 0x63, 0x31, 0xde, 0x16, 0xa1, 0x32, 0x7c, 0xc0, 0x82, 0x39, 0x60, 0x08, 0x1e, 0x3b, 0x95, 0xc9, 0x67,
0xd8, 0x68, 0x8a, 0x5d, 0x73, 0x61, 0xf0, 0x81, 0xaa, 0x2c, 0x24, 0x2f, 0xd6, 0xc9, 0xf6, 0xec, 0x14, 0x9e, 0x5f,
0xc6, 0x05, 0x50, 0x07, 0xd0, 0xaf, 0xa8, 0x2c, 0x36, 0x90, 0x8b, 0x9b, 0xb2, 0xd6, 0x2b, 0x1a, 0x8f, 0xaf, 0xed,
0xc2, 0xea, 0x0a, 0x7c, 0x1a, 0xa5, 0xe3, 0x44, 0x4c, 0x62, 0x26, 0x55, 0xae, 0xc9, 0xb5, 0xd1, 0xbd, 0xb4, 0x45,
0xf3, 0x5c, 0x48, 0xf0, 0x8a, 0xc0, 0x0d, 0xa1, 0xaf, 0xf4, 0xe5, 0x7a, 0x03, 0x05, 0x8f, 0xda, 0x9b, 0x8b, 0x60,
0x62, 0xe2, 0x31, 0x43, 0x6a, 0xfa, 0x75, 0x38, 0x15, 0xdf, 0xfc, 0xb6, 0xe2, 0xf0, 0xeb, 0x9c, 0xb1, 0x86, 0x02,
0x20, 0x3e, 0x79, 0x70, 0xb5, 0x9b, 0xf4, 0x4a, 0x69, 0x07, 0xa5, 0x11, 0xe2, 0xdb, 0x0a, 0x5f, 0x77, 0xa9, 0xf8,
0xca, 0x55, 0xf7, 0xbe, 0x8e, 0x99, 0x71, 0xc1, 0xe8, 0x39, 0x9f, 0x25, 0x8d, 0x6b, 0x37, 0x74, 0x57, 0xe7, 0x47,
0xef, 0x07, 0x99, 0xb7, 0x70, 0x0c, 0x6c, 0x72, 0xcc, 0x9c, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x5b, 0xf3, 0x88,
0x57, 0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xdb, 0xc1, 0xb7, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26,
0xca, 0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54,
0x44, 0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d,
0x46, 0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85,
0x77, 0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0,
0x74, 0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x74, 0xbe, 0x6a, 0x95, 0x58,
0xba, 0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f,
0xcd, 0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45,
0x90, 0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa,
0x66, 0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97,
0x97, 0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x23, 0x5b, 0x3f,
0x78, 0x73, 0xf7, 0x0a, 0x58, 0x3e, 0x02, 0x76, 0x1f, 0x99, 0xd3, 0x18, 0xaa, 0xda, 0xc0, 0x3f, 0xac, 0x1f, 0x6c,
0xdd, 0x1e, 0xfe, 0x61, 0xf0, 0x43, 0x70, 0x6d, 0x63, 0x63, 0x1b, 0x6f, 0xd7, 0x12, 0x41, 0x5e, 0xe1, 0x81, 0x3e,
0x5e, 0x7d, 0x14, 0xb4, 0x5c, 0x27, 0xb6, 0x07, 0x0e, 0x85, 0xad, 0x41, 0xbe, 0x49, 0x99, 0x34, 0x5a, 0x14, 0x3c,
0x9b, 0xc9, 0x19, 0x0a, 0x79, 0xcd, 0xc7, 0x41, 0xdb, 0x11, 0xfe, 0x06, 0x4e, 0xed, 0x78, 0x79, 0xf9, 0x09, 0xfa,
0x80, 0xa7, 0x2b, 0xa5, 0xa9, 0x88, 0x53, 0xca, 0x2d, 0xba, 0x5a, 0xe7, 0xc1, 0x48, 0x71, 0x31, 0x45, 0xa5, 0xe3,
0x2e, 0xaf, 0x9d, 0x8d, 0x9c, 0xfe, 0x12, 0xaf, 0x2e, 0xd2, 0xe5, 0x23, 0x91, 0xad, 0x5a, 0x7a, 0x2f, 0xf4, 0xe9,
0xb6, 0x3d, 0x63, 0x7c, 0x9a, 0x8d, 0xe9, 0x60, 0xc6, 0xc7, 0x89, 0xf0, 0xfa, 0xc4, 0x58, 0xdf, 0x2d, 0x02, 0xd3,
0xcd, 0xb1, 0xc9, 0x0f, 0xc7, 0xeb, 0xcd, 0x66, 0x8d, 0x3b, 0x78, 0xe3, 0x3c, 0x71, 0x96, 0x25, 0x46, 0x54, 0x96,
0x1a, 0x1e, 0xd0, 0x0a, 0x71, 0xf3, 0x9e, 0x09, 0x8c, 0xcb, 0x2e, 0x48, 0x6a, 0xbb, 0x81, 0xc0, 0xc5, 0x9e, 0xc4,
0x2c, 0x19, 0xdb, 0x1e, 0x94, 0x07, 0xfa, 0x62, 0x34, 0xdd, 0x02, 0xa6, 0xe5, 0xb5, 0xb3, 0xb3, 0xd4, 0xf6, 0xaa,
0xa9, 0x02, 0x98, 0x25, 0xcb, 0xe3, 0x13, 0x64, 0xdd, 0x6f, 0xa0, 0x8b, 0x18, 0x30, 0x36, 0xae, 0xcc, 0xb9, 0xcb,
0x75, 0x2b, 0xe2, 0x1b, 0x4d, 0xa4, 0x49, 0x7d, 0x48, 0x7d, 0x87, 0x61, 0xad, 0xae, 0x72, 0x90, 0xc0, 0x3d, 0xf2,
0x6e, 0x89, 0x4b, 0x4f, 0x9f, 0x59, 0x4c, 0xaa, 0xf4, 0x2d, 0x75, 0x2d, 0xae, 0x19, 0xf6, 0x8a, 0x07, 0x60, 0x7f,
0x60, 0xdc, 0x22, 0x16, 0xf1, 0x76, 0x5e, 0x4b, 0x61, 0x6d, 0xcc, 0x81, 0xe6, 0x86, 0x1b, 0xbc, 0x60, 0xd5, 0x9a,
0x81, 0x19, 0x66, 0x9c, 0x91, 0xfc, 0x66, 0xdc, 0xab, 0x9a, 0x38, 0x72, 0x15, 0x40, 0xf4, 0x2d, 0xe9, 0x92, 0x1c,
0x5e, 0xc9, 0x72, 0xd5, 0x19, 0xf2, 0xaf, 0xb0, 0xce, 0x7a, 0x71, 0x02, 0x66, 0xd2, 0x94, 0x97, 0x98, 0x98, 0x22,
0x2e, 0x37, 0xcb, 0x98, 0xa7, 0xe9, 0xb3, 0x68, 0x07, 0x27, 0x37, 0x12, 0x38, 0x62, 0xdf, 0x58, 0x86, 0x66, 0xc2,
0x46, 0x4c, 0xa4, 0x51, 0x29, 0x25, 0x7c, 0x20, 0x97, 0x5a, 0xf2, 0x97, 0xb9, 0xbc, 0xfa, 0x72, 0x9b, 0xe0, 0x80,
0xbc, 0x06, 0x96, 0x43, 0xe3, 0xb8, 0x65, 0x20, 0x11, 0x8b, 0x01, 0x31, 0x6a, 0x55, 0xae, 0x26, 0xa3, 0x3a, 0x99,
0xaf, 0x90, 0x0b, 0x15, 0x79, 0x70, 0x4b, 0xa0, 0xe4, 0xcf, 0x31, 0x75, 0x30, 0x2b, 0xb5, 0x9b, 0x16, 0x9b, 0x24,
0xef, 0x99, 0x01, 0xc9, 0xf5, 0xd7, 0xf0, 0xd0, 0xf8, 0xc5, 0x2b, 0x73, 0x4a, 0xf8, 0xa2, 0x8c, 0xa5, 0xa5, 0x31,
0x97, 0xfe, 0x83, 0xbc, 0x4f, 0x2b, 0x01, 0xfb, 0x15, 0xc4, 0x94, 0x81, 0x4b, 0x6c, 0x5c, 0x90, 0x94, 0xd7, 0xf2,
0x94, 0xdd, 0xd7, 0x50, 0xbe, 0x2b, 0x26, 0x5d, 0xa5, 0xb2, 0xae, 0xb0, 0xea, 0x7e, 0x5d, 0xb0, 0xfc, 0x62, 0x9f,
0x61, 0x6e, 0x32, 0x1a, 0x64, 0x2b, 0x66, 0x36, 0xe5, 0x57, 0x7b, 0xd7, 0x7e, 0xe5, 0xa1, 0xa4, 0x43, 0xb5, 0x4a,
0x37, 0xaf, 0xdc, 0x70, 0x8c, 0x1b, 0x37, 0x1c, 0x01, 0x6c, 0x0c, 0x3b, 0x55, 0xa4, 0xd6, 0xf9, 0xef, 0xab, 0xe1,
0x27, 0xda, 0x6b, 0x43, 0xbd, 0xeb, 0x86, 0x6b, 0xd3, 0xd3, 0xaf, 0x41, 0xd5, 0xc8, 0x12, 0xba, 0x0e, 0x55, 0x4c,
0x46, 0xa2, 0xc4, 0x74, 0x95, 0xf2, 0xa8, 0xaf, 0x11, 0xe7, 0x20, 0x6e, 0x28, 0x7f, 0xf1, 0x2f, 0xe1, 0xc5, 0x51,
0x80, 0x46, 0xd4, 0x72, 0x92, 0xa5, 0xbc, 0x35, 0x89, 0x66, 0x71, 0x72, 0x11, 0x2c, 0xe2, 0xd6, 0x2c, 0x4b, 0xb3,
0x62, 0x0e, 0x5c, 0xe9, 0x15, 0x17, 0x60, 0xc3, 0xcf, 0x5a, 0x8b, 0xd8, 0x7b, 0xce, 0x92, 0x53, 0xc6, 0xe3, 0x51,
0xe4, 0xd9, 0x7b, 0x39, 0x88, 0x07, 0xeb, 0x75, 0x94, 0xe7, 0xd9, 0x99, 0xed, 0xbd, 0xcb, 0x8e, 0x81, 0x69, 0xbd,
0x37, 0xe7, 0x17, 0x27, 0x2c, 0xf5, 0xde, 0x1f, 0x2f, 0x52, 0xbe, 0xf0, 0x8a, 0x28, 0x2d, 0x5a, 0x05, 0xcb, 0xe3,
0x09, 0xa8, 0x89, 0x24, 0xcb, 0x5b, 0x98, 0xff, 0x3c, 0x63, 0x41, 0x12, 0x9f, 0x4c, 0xb9, 0x35, 0x8e, 0xf2, 0x4f,
0xbd, 0x56, 0x6b, 0x9e, 0xc7, 0xb3, 0x28, 0xbf, 0x68, 0x51, 0x8b, 0xe0, 0x8b, 0xf6, 0x76, 0xf4, 0xe5, 0xe4, 0x7e,
0x8f, 0xe7, 0xd0, 0x37, 0x46, 0x2a, 0x06, 0x20, 0x7c, 0xac, 0xed, 0x9d, 0xf6, 0xac, 0xb8, 0x23, 0x4e, 0x94, 0xa2,
0x94, 0x97, 0x47, 0xde, 0x19, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79,
0x01, 0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04,
0xf7, 0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66,
0x05, 0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2,
0x93, 0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff,
0xad, 0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39,
0xf8, 0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe,
0x5e, 0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01,
0xc8, 0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e,
0x8e, 0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f,
0xc5, 0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98,
0xe8, 0xab, 0xab, 0x3e, 0x62, 0xf3, 0xdd, 0x8d, 0x5f, 0xaa, 0xf1, 0x2e, 0x54, 0xde, 0xa0, 0x50, 0x11, 0xea, 0x9b,
0x2d, 0x98, 0xf1, 0x96, 0xf7, 0x3b, 0xfa, 0xa0, 0x6a, 0xf0, 0x1d, 0x23, 0xad, 0x17, 0x70, 0xcf, 0xcc, 0x05, 0xea,
0xa5, 0x7d, 0x0c, 0x49, 0xb5, 0x5a, 0x2e, 0xe8, 0x0d, 0x86, 0x21, 0x24, 0x3a, 0x10, 0x74, 0xf2, 0x41, 0x41, 0xdf,
0xd4, 0xc8, 0xdc, 0xa0, 0x70, 0x32, 0x17, 0xb6, 0x7c, 0xa6, 0xe5, 0x3a, 0x28, 0x69, 0xf0, 0xb2, 0xbf, 0x62, 0xb2,
0x01, 0x48, 0xef, 0x4a, 0xd2, 0x7e, 0xaf, 0x4f, 0x9e, 0x94, 0xc7, 0x97, 0x8d, 0x88, 0x70, 0xe0, 0xea, 0xf3, 0x29,
0xba, 0xdd, 0xfa, 0x3b, 0x31, 0x46, 0x46, 0xcd, 0x96, 0xed, 0x0e, 0x98, 0x4e, 0xca, 0xc2, 0xe4, 0x33, 0x56, 0xe2,
0x28, 0x5f, 0xb3, 0xf0, 0x7b, 0x0c, 0xbc, 0xb2, 0x50, 0x78, 0x69, 0xca, 0x47, 0x9b, 0x5d, 0x77, 0xfb, 0x1f, 0x16,
0x3c, 0xa6, 0x64, 0xe7, 0xc3, 0xe1, 0xbf, 0xc1, 0x67, 0x70, 0x34, 0x06, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0x04, 0x5c,
0x0d, 0x26, 0xd8, 0xa4, 0xcb, 0x6d, 0x8f, 0x69, 0x15, 0x82, 0x1e, 0x76, 0xed, 0xfb, 0x09, 0x9c, 0xce, 0x57, 0xc4,
0xf5, 0x05, 0x19, 0x40, 0x51, 0x10, 0xa2, 0x56, 0x1c, 0x53, 0x2a, 0x1d, 0x5d, 0xed, 0xf4, 0x17, 0x69, 0x0c, 0x42,
0xf4, 0x63, 0x3c, 0xa6, 0x2b, 0x2d, 0xf1, 0x98, 0xce, 0x38, 0x5a, 0x94, 0xd3, 0x84, 0x41, 0x73, 0x28, 0x90, 0xb4,
0xc5, 0x67, 0x99, 0x23, 0x63, 0xb7, 0x6c, 0x3c, 0xa7, 0x30, 0xb4, 0xf0, 0x38, 0x9b, 0x45, 0x71, 0x1a, 0xe0, 0xa7,
0x47, 0x3c, 0x3d, 0x62, 0x80, 0x5d, 0x3c, 0xf8, 0xa9, 0xc8, 0xdc, 0x71, 0xfd, 0x5f, 0x40, 0x44, 0x51, 0xff, 0x52,
0xba, 0x78, 0x1a, 0x2e, 0x75, 0x82, 0x5c, 0x2f, 0x05, 0xb1, 0xc6, 0x95, 0x39, 0xcc, 0x28, 0x84, 0xb2, 0xcb, 0xe9,
0xc7, 0xa0, 0xd5, 0x09, 0x3a, 0xda, 0x2b, 0xae, 0x5d, 0x74, 0x15, 0x69, 0x32, 0xf2, 0xb2, 0x24, 0xc1, 0xa0, 0x9f,
0x05, 0x9c, 0xd5, 0xbb, 0x86, 0xd5, 0x93, 0x2c, 0x8f, 0xb1, 0xa1, 0x93, 0xd4, 0xa9, 0x01, 0x41, 0xc7, 0x0c, 0x57,
0x4c, 0xe5, 0x96, 0x11, 0x31, 0xe1, 0x63, 0x92, 0x0d, 0xf5, 0x6b, 0x4a, 0x78, 0x25, 0xa9, 0x9e, 0x5d, 0xa5, 0xb3,
0xa4, 0x8f, 0x76, 0x85, 0x30, 0xb1, 0x88, 0xc7, 0x42, 0x1b, 0x76, 0xb7, 0x6d, 0xfd, 0x79, 0x04, 0x54, 0xfa, 0x14,
0xda, 0x1b, 0x4b, 0x47, 0xe5, 0xec, 0xe7, 0x30, 0xd7, 0x9e, 0x50, 0xa8, 0x74, 0xd9, 0xdf, 0xee, 0x6f, 0x2c, 0x79,
0xb9, 0xbb, 0x25, 0x7a, 0xf7, 0x8f, 0xca, 0x82, 0x74, 0x9f, 0x19, 0xa3, 0xab, 0xa6, 0x10, 0x75, 0x30, 0x2c, 0x65,
0x06, 0xe3, 0xb8, 0xb9, 0xb6, 0xe7, 0x44, 0x30, 0x5a, 0xb2, 0x5d, 0x81, 0x99, 0x50, 0x51, 0x0e, 0xdb, 0x5d, 0xe7,
0xba, 0x14, 0x22, 0xbd, 0xa3, 0xb7, 0x0a, 0xc5, 0x11, 0x42, 0x30, 0xd8, 0x58, 0xc6, 0x65, 0xb8, 0xb1, 0x64, 0xe9,
0x28, 0x1b, 0xb3, 0xf7, 0xef, 0x5e, 0xe0, 0x75, 0x86, 0x2c, 0x45, 0xb9, 0x97, 0xb9, 0xe5, 0x11, 0x18, 0x42, 0x08,
0x69, 0xae, 0xbe, 0x26, 0x03, 0xc0, 0x88, 0x98, 0x8e, 0x45, 0xa3, 0x22, 0x28, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x10,
0x1c, 0x4e, 0x2c, 0x00, 0x53, 0x91, 0x7a, 0x31, 0xc0, 0x4f, 0xb4, 0xee, 0xc3, 0x40, 0xbb, 0x5b, 0xa2, 0x11, 0xe0,
0x9a, 0x23, 0x1a, 0x15, 0xaa, 0x98, 0xfd, 0x63, 0xa2, 0x3b, 0x8e, 0x4f, 0x35, 0x39, 0x29, 0x15, 0xba, 0xbf, 0x9b,
0x44, 0xc7, 0x2c, 0x81, 0x21, 0x8b, 0xcb, 0xcb, 0x36, 0x8c, 0x24, 0x5e, 0xad, 0xdd, 0x38, 0x9d, 0x2f, 0xe4, 0x57,
0xbd, 0x60, 0xe2, 0x0e, 0x1e, 0xb4, 0xe2, 0xa5, 0x83, 0x81, 0x3a, 0x39, 0x0c, 0xe4, 0x00, 0x00, 0x22, 0x1d, 0x5a,
0x20, 0x74, 0x15, 0xab, 0x40, 0x69, 0x3c, 0x5e, 0x2d, 0x83, 0xdd, 0x39, 0xc7, 0xd2, 0x14, 0x9e, 0x67, 0x71, 0x8a,
0x8f, 0x05, 0x3e, 0x46, 0xe7, 0xf8, 0x98, 0xc1, 0xa3, 0xc6, 0x3d, 0x2f, 0xed, 0x7f, 0xd7, 0x55, 0xc9, 0xe4, 0x0a,
0x58, 0x9a, 0x00, 0xd9, 0xe5, 0x25, 0xa8, 0x17, 0x4d, 0x82, 0xdd, 0x2d, 0x20, 0x16, 0x72, 0x8f, 0xf8, 0xc6, 0x0b,
0x33, 0xc9, 0xc8, 0x8a, 0x79, 0x4b, 0x94, 0x5b, 0xa4, 0xc4, 0x43, 0xf0, 0xf1, 0x72, 0xa7, 0x61, 0xab, 0x78, 0x32,
0x9b, 0xe5, 0x09, 0xbe, 0xb8, 0xb6, 0x25, 0x3e, 0xc2, 0x21, 0x88, 0x42, 0x8f, 0x88, 0xa1, 0x2e, 0xe3, 0xf2, 0xf3,
0x3a, 0x71, 0x68, 0xe3, 0x2c, 0x60, 0x2e, 0xa2, 0xd2, 0xe1, 0x51, 0x9c, 0x88, 0xc6, 0x6b, 0xf0, 0x69, 0xa4, 0x25,
0x12, 0x3a, 0xbb, 0x5b, 0x15, 0x6c, 0x00, 0xfc, 0x48, 0x5c, 0xea, 0x76, 0x44, 0xca, 0xa5, 0x2d, 0xca, 0xe9, 0xa8,
0x5e, 0x6e, 0x73, 0x19, 0x48, 0x16, 0xa1, 0x79, 0x8d, 0x2a, 0xa5, 0x48, 0xda, 0x93, 0x28, 0x5d, 0xd7, 0x14, 0xa0,
0x9f, 0x33, 0x36, 0xf6, 0x6c, 0x0b, 0xe4, 0xab, 0x78, 0xfe, 0x98, 0xb0, 0x53, 0x26, 0x3f, 0xcc, 0xa2, 0x07, 0xd1,
0x95, 0x23, 0xb0, 0x00, 0xb8, 0xbc, 0x33, 0x2a, 0xd9, 0x53, 0xe1, 0x28, 0x29, 0x51, 0x47, 0xc4, 0xb3, 0x8d, 0x41,
0x9b, 0x73, 0xb4, 0xeb, 0xc3, 0x7a, 0xa0, 0x93, 0x6c, 0x5b, 0xc0, 0x4b, 0x66, 0xe3, 0xcd, 0xc8, 0xc1, 0x00, 0x6c,
0x32, 0x81, 0xed, 0xa2, 0x02, 0xd9, 0x5a, 0x3e, 0x08, 0xa8, 0xc0, 0x07, 0xf6, 0x6c, 0xd1, 0xc2, 0xae, 0xda, 0xea,
0x27, 0x49, 0x1c, 0xd0, 0x9f, 0x9f, 0xd6, 0xcc, 0x05, 0xdc, 0xb1, 0x21, 0x02, 0xbb, 0x0d, 0x99, 0x67, 0xff, 0xeb,
0x3f, 0xfd, 0xaf, 0xff, 0x06, 0x03, 0x53, 0x3f, 0xb7, 0xb4, 0xae, 0x6e, 0xf5, 0x3f, 0xa1, 0xd5, 0x22, 0xbd, 0xa1,
0xdd, 0x5f, 0xff, 0xe1, 0xbf, 0x43, 0x33, 0xba, 0x00, 0x04, 0xb2, 0x84, 0x20, 0x1a, 0xa1, 0xcd, 0xf5, 0x59, 0x20,
0xd5, 0x06, 0xb9, 0x72, 0xa6, 0x7f, 0x44, 0xb0, 0x0b, 0x9e, 0xcd, 0xaf, 0x05, 0x07, 0xa1, 0x1e, 0x25, 0x59, 0xc1,
0x34, 0x3c, 0x42, 0xfe, 0x7e, 0x1e, 0x40, 0x34, 0xd7, 0x1c, 0xb8, 0xbc, 0xb0, 0xf4, 0x38, 0x62, 0x79, 0xd6, 0x8d,
0xd3, 0x58, 0xbd, 0x82, 0x71, 0x42, 0x87, 0xc2, 0x18, 0xb0, 0x5e, 0xe2, 0x09, 0x1e, 0x48, 0x20, 0xb8, 0xf5, 0x8f,
0x8a, 0xad, 0x1f, 0x4c, 0xf3, 0xa7, 0x18, 0x4b, 0x44, 0x6e, 0xd4, 0x08, 0xf0, 0x13, 0x84, 0xd6, 0x47, 0xfd, 0x1c,
0x9d, 0xeb, 0x67, 0x14, 0x6c, 0x62, 0x02, 0xd0, 0x4f, 0x33, 0x34, 0x3d, 0xcc, 0x19, 0x44, 0xd6, 0x41, 0xe5, 0xb6,
0x1a, 0xc9, 0x2b, 0x42, 0x78, 0x7d, 0xc4, 0x1c, 0x4f, 0xbc, 0xd1, 0xb3, 0xc8, 0xd9, 0xc7, 0x24, 0x3b, 0xc3, 0x58,
0x18, 0x12, 0xe9, 0xaa, 0xfa, 0xf2, 0x5f, 0xfe, 0xd9, 0xf7, 0xff, 0xe5, 0x9f, 0xaf, 0x68, 0x30, 0x85, 0x7d, 0x00,
0xc6, 0x2b, 0x0f, 0x35, 0x9d, 0x1b, 0x68, 0xad, 0x1f, 0x14, 0xf1, 0x5c, 0x5f, 0x23, 0x11, 0xc7, 0x52, 0x89, 0xb7,
0x7c, 0x24, 0xb4, 0x35, 0x53, 0xdc, 0x3c, 0x0b, 0x42, 0x76, 0xc5, 0x34, 0x58, 0x75, 0xc3, 0x3c, 0x47, 0x6e, 0x70,
0x0d, 0x5d, 0x3e, 0x13, 0xe3, 0xf5, 0x60, 0xdc, 0x08, 0x81, 0x07, 0xd2, 0x5f, 0xe8, 0x87, 0x27, 0x42, 0xba, 0x07,
0x62, 0x19, 0xa4, 0xac, 0xaf, 0x01, 0xe4, 0x59, 0x07, 0x34, 0x01, 0x35, 0x89, 0x2b, 0xdd, 0x4a, 0xe6, 0xc9, 0x71,
0xde, 0x7f, 0x85, 0x97, 0xf8, 0x2c, 0xec, 0x8d, 0x9a, 0xb5, 0x20, 0x43, 0x80, 0x93, 0x21, 0x20, 0x35, 0x90, 0xa9,
0x83, 0xd1, 0xa1, 0xc9, 0xd6, 0xeb, 0xda, 0x8f, 0xd8, 0xbd, 0xa6, 0x2d, 0xb9, 0xd4, 0x96, 0xb1, 0xb4, 0x6e, 0xa5,
0xb6, 0xc4, 0x4f, 0xb5, 0x34, 0xb4, 0x65, 0x7c, 0xa5, 0xb6, 0x44, 0xca, 0x0d, 0x70, 0xe4, 0xd0, 0xde, 0xc4, 0xa8,
0x8d, 0xa1, 0x9b, 0x39, 0xda, 0x25, 0xe0, 0xaa, 0x8f, 0x3e, 0x49, 0xb3, 0x84, 0x10, 0xc0, 0xb0, 0x85, 0x36, 0xbe,
0x04, 0x06, 0xa0, 0x62, 0x8f, 0x4a, 0xbd, 0xe9, 0xf1, 0xd1, 0x98, 0x80, 0xbb, 0xcb, 0x09, 0x43, 0x91, 0x0c, 0x6b,
0xf6, 0x35, 0x2b, 0xb7, 0x70, 0x1c, 0xb1, 0x61, 0xc4, 0x33, 0x60, 0xb6, 0x85, 0x83, 0x1d, 0x79, 0x4b, 0x11, 0x04,
0x0b, 0xec, 0xb7, 0x6f, 0xf6, 0x0f, 0x6c, 0xef, 0x38, 0x1b, 0x5f, 0x04, 0x36, 0x78, 0x19, 0x60, 0x7d, 0xb8, 0x3e,
0x9f, 0xb2, 0xd4, 0x51, 0x76, 0x7e, 0x96, 0x80, 0x0b, 0x95, 0x9d, 0x88, 0x6f, 0x36, 0x34, 0x03, 0xf8, 0x40, 0x58,
0xfa, 0xa8, 0x63, 0x7f, 0x97, 0x8b, 0xef, 0x9d, 0xf2, 0x1c, 0x1f, 0xfb, 0x98, 0xc2, 0xb0, 0xbb, 0x05, 0x0f, 0xf8,
0xb2, 0x8f, 0xfa, 0x48, 0xbf, 0x09, 0x60, 0x0b, 0xf1, 0xbe, 0x85, 0xed, 0xb7, 0x54, 0x5f, 0x84, 0xa2, 0x2f, 0xb9,
0x4d, 0x9b, 0xe2, 0x95, 0x2d, 0x47, 0x63, 0x8f, 0xd1, 0x54, 0x23, 0x53, 0x1c, 0x48, 0xc2, 0xc7, 0xba, 0x44, 0xa8,
0x7f, 0xa3, 0x88, 0x46, 0xa9, 0x94, 0xb5, 0xac, 0xc2, 0x09, 0xc9, 0xbc, 0x13, 0x93, 0xc1, 0x4f, 0x02, 0xff, 0xc8,
0xfc, 0x3e, 0x99, 0xf8, 0x94, 0x90, 0x46, 0xf2, 0xf0, 0x2f, 0xde, 0x19, 0xf3, 0x2e, 0x8e, 0xa8, 0xa5, 0x72, 0x86,
0x31, 0x4a, 0x83, 0x41, 0x83, 0xb6, 0x8a, 0x7a, 0x80, 0x7d, 0x92, 0x44, 0xf3, 0x82, 0x05, 0xea, 0x41, 0xfa, 0xc5,
0xe8, 0x5e, 0xaf, 0x06, 0x22, 0x4c, 0x3b, 0xa6, 0xe4, 0xd3, 0xa5, 0xe9, 0x38, 0x3f, 0x00, 0xc7, 0x19, 0x93, 0xfe,
0x5b, 0x11, 0x68, 0xdf, 0x34, 0x48, 0xd8, 0x84, 0x97, 0x1c, 0x6f, 0x95, 0x2f, 0x55, 0xa4, 0xc2, 0xef, 0xee, 0x80,
0x33, 0x6d, 0xf9, 0xf8, 0xff, 0x4d, 0x63, 0x8f, 0x83, 0x14, 0x9c, 0x5f, 0xba, 0x66, 0x82, 0x57, 0xf8, 0x00, 0x22,
0xf3, 0x7d, 0x69, 0x4c, 0x34, 0x62, 0x18, 0xc5, 0x29, 0x79, 0x0e, 0x72, 0xdb, 0xe3, 0xb9, 0xd9, 0x0e, 0xe4, 0xed,
0x4a, 0x28, 0x67, 0x35, 0x98, 0xb0, 0xed, 0x4a, 0xbf, 0x60, 0xb5, 0xb1, 0x8a, 0xc8, 0xd4, 0xdf, 0x56, 0x28, 0x64,
0xc4, 0x8e, 0x4a, 0xa1, 0x6a, 0x96, 0xa2, 0x87, 0x89, 0xd3, 0x6a, 0x54, 0xe9, 0x46, 0x4b, 0xb1, 0xa4, 0x6d, 0x7f,
0x48, 0xdb, 0x9e, 0xc4, 0xd8, 0x70, 0x29, 0xe6, 0x1e, 0x45, 0xc9, 0xc8, 0x41, 0x00, 0xac, 0x96, 0xf5, 0x08, 0xa8,
0xe9, 0xaa, 0xc8, 0x86, 0xff, 0x10, 0x89, 0x5b, 0x0a, 0xa1, 0xb7, 0x86, 0x4a, 0x47, 0xc3, 0xb2, 0xec, 0x5d, 0x30,
0xe7, 0xf0, 0x37, 0x79, 0x49, 0x41, 0xdc, 0x4d, 0xd5, 0xdf, 0xf7, 0x6b, 0x97, 0xee, 0x10, 0x9c, 0x79, 0xe3, 0xab,
0x69, 0xb6, 0xe2, 0x68, 0xdb, 0xeb, 0x92, 0x9f, 0x8f, 0xbd, 0x5f, 0x39, 0x36, 0x1a, 0x97, 0x54, 0x75, 0xd1, 0x22,
0x0e, 0xb3, 0xa9, 0xa3, 0x88, 0xb2, 0x7f, 0x73, 0x55, 0xb0, 0xe4, 0xdb, 0x9b, 0x83, 0x25, 0xfc, 0x96, 0xc1, 0x92,
0x6f, 0xff, 0xe0, 0x60, 0xc9, 0x37, 0x66, 0xb0, 0x04, 0x65, 0xe5, 0x8b, 0xcf, 0x89, 0x68, 0xe4, 0xd9, 0x59, 0x11,
0x76, 0xe4, 0xe1, 0x3c, 0x88, 0x9d, 0xff, 0x98, 0xd0, 0x06, 0x4c, 0xd4, 0x08, 0x6c, 0x50, 0x24, 0x43, 0xe4, 0x13,
0x82, 0x84, 0x97, 0x71, 0x84, 0xb6, 0x4e, 0xdc, 0x6b, 0xdd, 0x57, 0x37, 0x95, 0xc0, 0x6b, 0x73, 0x75, 0x38, 0xa7,
0xab, 0x22, 0x0d, 0x01, 0x7d, 0x6a, 0x54, 0x77, 0xec, 0x6e, 0xaa, 0x8c, 0x31, 0x73, 0x84, 0x9e, 0x3a, 0x08, 0x10,
0x1c, 0xb4, 0xb4, 0xff, 0xf3, 0x61, 0xa7, 0xb7, 0xdd, 0x99, 0x41, 0x6f, 0xd0, 0xa5, 0xf0, 0xd6, 0xee, 0x6d, 0x6f,
0xe3, 0xdb, 0x99, 0x7a, 0xeb, 0xe2, 0x5b, 0xac, 0xde, 0x76, 0xf0, 0x6d, 0xa4, 0xde, 0x1e, 0xe0, 0xdb, 0x58, 0xbd,
0x3d, 0xc4, 0xb7, 0x53, 0xbb, 0x3c, 0xe4, 0x1a, 0xb8, 0x87, 0xc0, 0x58, 0x64, 0xf0, 0x07, 0xaa, 0x0c, 0xf6, 0x2d,
0xde, 0xf0, 0x8b, 0x4e, 0x82, 0xd8, 0x13, 0x8e, 0x51, 0x90, 0x7b, 0x67, 0x20, 0xfc, 0x03, 0x4a, 0x68, 0x7b, 0x8a,
0x9f, 0x7a, 0x00, 0x3f, 0xe2, 0x20, 0x9e, 0x31, 0xf5, 0xcd, 0x5b, 0x85, 0x35, 0xd8, 0x92, 0x87, 0xed, 0x61, 0xd9,
0xd3, 0xeb, 0x24, 0x02, 0x29, 0x2a, 0x61, 0x80, 0x56, 0xae, 0xaa, 0x13, 0xd3, 0xb5, 0xf4, 0x0a, 0x5f, 0xa1, 0x4a,
0x0c, 0x57, 0x7a, 0x02, 0x36, 0x52, 0xeb, 0x1c, 0x9c, 0xaf, 0xb5, 0xea, 0x05, 0x21, 0xd2, 0x0a, 0x85, 0x70, 0xd2,
0x6f, 0x07, 0xd1, 0x89, 0x7e, 0x7e, 0x05, 0x46, 0x6f, 0x74, 0xc2, 0x6e, 0x52, 0x35, 0x04, 0xa2, 0xa9, 0x66, 0x14,
0x10, 0x64, 0x15, 0xc1, 0xd2, 0xa0, 0x13, 0x28, 0xd5, 0x0c, 0x52, 0xa7, 0xae, 0x78, 0x68, 0xfa, 0x7a, 0x11, 0x50,
0xb4, 0x2a, 0xd8, 0x05, 0xdb, 0x9b, 0x4a, 0x05, 0x85, 0xa1, 0x02, 0x0b, 0xae, 0xd5, 0x46, 0xda, 0x4f, 0xbe, 0x52,
0x27, 0x59, 0x4a, 0x1d, 0x99, 0x07, 0x08, 0xfa, 0xf4, 0x60, 0x55, 0x42, 0x7e, 0xd1, 0x19, 0xe1, 0x1f, 0x29, 0x7f,
0xbf, 0x98, 0x4c, 0x26, 0xd7, 0xaa, 0xa7, 0x2f, 0xc6, 0x13, 0xd6, 0x65, 0x3b, 0x3d, 0x0c, 0x2e, 0xb7, 0xa4, 0x44,
0xec, 0x94, 0x44, 0xbb, 0xe5, 0xed, 0x1a, 0xa3, 0xf0, 0x04, 0x8d, 0x75, 0x7b, 0x3d, 0x56, 0x02, 0x55, 0x96, 0x20,
0xbf, 0x4f, 0xe2, 0x34, 0x68, 0x97, 0xfe, 0xa9, 0x14, 0xfc, 0x5f, 0x3c, 0x7a, 0xf4, 0xa8, 0xf4, 0xc7, 0xea, 0xad,
0x3d, 0x1e, 0x97, 0xfe, 0x68, 0xa9, 0xd1, 0x68, 0xb7, 0x27, 0x93, 0xd2, 0x8f, 0x55, 0xc1, 0x76, 0x77, 0x34, 0xde,
0xee, 0x96, 0xfe, 0x99, 0xd1, 0xa2, 0xf4, 0x99, 0x7c, 0xcb, 0xd9, 0xb8, 0x16, 0xa1, 0x7e, 0xd8, 0x86, 0x4a, 0xc1,
0x68, 0x4b, 0x74, 0xf4, 0xc4, 0x63, 0x10, 0x2d, 0x78, 0x06, 0x36, 0x16, 0xf0, 0x36, 0x08, 0xe8, 0x89, 0x14, 0xef,
0xe2, 0x93, 0xb2, 0x28, 0xd4, 0x5f, 0x98, 0x32, 0x1d, 0x99, 0x99, 0xe4, 0x39, 0x27, 0x55, 0xd0, 0xac, 0x46, 0xce,
0xa2, 0xea, 0x17, 0x21, 0xaf, 0xa4, 0x3d, 0x4a, 0x1b, 0x6c, 0x29, 0x64, 0xfc, 0x8f, 0x57, 0xc9, 0xf8, 0x1f, 0x6e,
0x96, 0xf1, 0xc7, 0xb7, 0x13, 0xf1, 0x3f, 0xfc, 0xc1, 0x22, 0xfe, 0x47, 0x53, 0xc4, 0x0b, 0x21, 0xb6, 0x07, 0x56,
0x2c, 0x99, 0xaf, 0xc7, 0xd9, 0x79, 0x0b, 0xb7, 0x44, 0x6e, 0x93, 0xf4, 0xdc, 0xb8, 0x95, 0xf0, 0x5f, 0x93, 0x72,
0xa4, 0x06, 0x33, 0xbe, 0x4f, 0xcb, 0xb3, 0x93, 0x93, 0x84, 0x29, 0x19, 0x6f, 0x54, 0x90, 0x65, 0xfc, 0x26, 0x0d,
0xed, 0x37, 0xe0, 0xa4, 0x1a, 0x25, 0x93, 0x09, 0x14, 0x4d, 0x26, 0xb6, 0x4a, 0xc9, 0x05, 0x79, 0x46, 0xad, 0x5e,
0xd7, 0x4a, 0xa8, 0xd5, 0x57, 0x5f, 0x99, 0x65, 0x66, 0x81, 0x8c, 0x46, 0x99, 0xf6, 0x84, 0xac, 0x19, 0xc7, 0x05,
0xee, 0xc1, 0xea, 0x7b, 0xba, 0x68, 0xb2, 0xcc, 0x40, 0xa9, 0xc4, 0x23, 0xfc, 0x50, 0x4b, 0xf3, 0xdb, 0x25, 0x22,
0x7d, 0x7a, 0x15, 0xb9, 0xea, 0x88, 0xd5, 0xf8, 0x4c, 0x5e, 0x75, 0xac, 0x0a, 0x8b, 0x2f, 0x53, 0x28, 0x1e, 0x5f,
0xbc, 0x18, 0x3b, 0x7b, 0x60, 0xca, 0xc6, 0xc5, 0x9b, 0xb4, 0x91, 0x9a, 0x26, 0xc0, 0x0e, 0x43, 0x13, 0xd3, 0x52,
0x10, 0xac, 0xca, 0xd1, 0xaf, 0x2a, 0x7b, 0x46, 0x27, 0x99, 0xc1, 0x84, 0x43, 0x0e, 0x6a, 0x64, 0x09, 0xcc, 0xc1,
0xa4, 0x2e, 0xa4, 0xef, 0xa5, 0x8b, 0xbc, 0x8e, 0x53, 0xf9, 0x61, 0x6d, 0x3a, 0x15, 0x58, 0x4a, 0xfd, 0x21, 0x4f,
0xfc, 0xab, 0x9e, 0x18, 0x88, 0x17, 0x33, 0x8c, 0x4b, 0x15, 0x7c, 0x07, 0xc2, 0xcd, 0xf1, 0x2b, 0x40, 0x62, 0x08,
0x15, 0xe3, 0xae, 0xa8, 0x77, 0x79, 0x69, 0x7e, 0x8c, 0xb2, 0xf6, 0xe1, 0xc2, 0x06, 0x0f, 0x30, 0xfc, 0x8a, 0x2c,
0x6a, 0x83, 0x6c, 0xc1, 0x1d, 0x87, 0x5a, 0x39, 0x6e, 0xe9, 0xed, 0xb4, 0xdb, 0xa0, 0x62, 0x7c, 0xf1, 0xa9, 0x21,
0x47, 0x77, 0x96, 0xf8, 0x5e, 0x15, 0x52, 0x5f, 0xf9, 0xf4, 0x97, 0x26, 0x31, 0x7e, 0x9b, 0x44, 0x20, 0x6a, 0x5c,
0x4f, 0x51, 0x8b, 0xd8, 0x7c, 0xf7, 0x95, 0x1b, 0x67, 0x10, 0xd6, 0x5d, 0xc7, 0xc1, 0x32, 0x56, 0x56, 0x2f, 0xc4,
0xb6, 0xc2, 0xaa, 0x59, 0x05, 0xe7, 0x06, 0x9d, 0x59, 0x9c, 0x19, 0xb1, 0xe7, 0xda, 0x36, 0x28, 0x55, 0xf0, 0x59,
0x44, 0x90, 0xf7, 0x30, 0x4e, 0x2a, 0x7c, 0x60, 0x05, 0x74, 0xdd, 0xfb, 0x34, 0x20, 0x47, 0xbf, 0x54, 0x33, 0xba,
0xaa, 0x52, 0x05, 0xa5, 0x79, 0x7a, 0x60, 0x20, 0x43, 0x41, 0x60, 0x58, 0xe3, 0x54, 0xe8, 0x2d, 0x98, 0x86, 0x04,
0xb0, 0x76, 0xca, 0xd0, 0x33, 0xb1, 0x15, 0xd8, 0x42, 0x5a, 0x80, 0xd2, 0xc3, 0x0e, 0x7d, 0xab, 0x06, 0x7a, 0xba,
0x1a, 0x3b, 0xbe, 0xce, 0x4f, 0xbb, 0x38, 0xf2, 0x8b, 0x33, 0x0f, 0xfe, 0x59, 0x7f, 0x5a, 0x82, 0x94, 0x3f, 0xfe,
0x14, 0x73, 0x30, 0xaa, 0xe7, 0x2d, 0x8c, 0x84, 0x50, 0x28, 0x53, 0xaa, 0x43, 0x3a, 0xed, 0x14, 0xb7, 0x92, 0x7a,
0x8b, 0x02, 0xdd, 0x39, 0xf2, 0x5b, 0x82, 0x34, 0x4b, 0x59, 0xaf, 0x7e, 0xaa, 0x6d, 0xba, 0x0e, 0x8a, 0x58, 0xc3,
0x65, 0x86, 0xee, 0x1f, 0xbf, 0x00, 0xf7, 0x4f, 0xa8, 0xd1, 0xb6, 0xf2, 0x1b, 0xda, 0x6b, 0xdb, 0x07, 0x92, 0xb6,
0x9b, 0x64, 0x2d, 0xe4, 0xab, 0xfe, 0xd1, 0x55, 0xfe, 0xcd, 0x4d, 0x67, 0xbc, 0xdd, 0x9d, 0x1d, 0x4f, 0xfd, 0x33,
0x0e, 0xc7, 0x9b, 0xc5, 0x74, 0xc6, 0x7b, 0x1b, 0xc8, 0x82, 0x68, 0x82, 0x1f, 0xf1, 0xbb, 0x4d, 0xcb, 0x63, 0x4a,
0xb0, 0x5c, 0xa2, 0x5a, 0x0f, 0x3a, 0x8f, 0xc0, 0x61, 0xbb, 0xf5, 0xf0, 0xd7, 0xa3, 0x5f, 0x4a, 0x1a, 0xa9, 0x7b,
0xb2, 0xb6, 0xdd, 0x43, 0x79, 0x91, 0x44, 0x17, 0xe0, 0x37, 0x92, 0x8d, 0x71, 0x8c, 0x81, 0xdc, 0xde, 0x3c, 0x93,
0xc9, 0x0a, 0x39, 0x4b, 0xe8, 0x37, 0x65, 0xc8, 0xa5, 0xd8, 0x7e, 0x30, 0x3f, 0x57, 0xab, 0xd1, 0x69, 0x24, 0x21,
0xfc, 0xa1, 0xb9, 0x06, 0x57, 0x27, 0x37, 0xd4, 0xcf, 0xcb, 0x77, 0x00, 0x06, 0x61, 0xd8, 0xb4, 0x72, 0x01, 0x55,
0x1b, 0x4a, 0x8c, 0x6c, 0x8b, 0x6a, 0x20, 0xcb, 0xdf, 0x06, 0x55, 0x19, 0x15, 0xac, 0x87, 0x5f, 0x53, 0x8c, 0xc1,
0x3b, 0x95, 0xc6, 0xd3, 0x2c, 0x1e, 0x8f, 0x13, 0xd6, 0x53, 0xf6, 0x91, 0xd5, 0x79, 0x80, 0xc9, 0x0a, 0xe6, 0x92,
0xd5, 0x57, 0xc5, 0x20, 0x9e, 0xa6, 0x53, 0x74, 0x0c, 0xf6, 0x1a, 0x7e, 0x12, 0x71, 0x2d, 0x39, 0xe5, 0x29, 0x7e,
0xbb, 0x22, 0x1e, 0x3d, 0xd7, 0x71, 0xd9, 0x01, 0x63, 0x91, 0x16, 0xbc, 0xdd, 0xe3, 0xd9, 0x3c, 0x68, 0x6d, 0xd7,
0x11, 0xc1, 0x2a, 0x8d, 0x82, 0xb7, 0x06, 0x2d, 0x0f, 0xad, 0x03, 0xa1, 0xe5, 0x2c, 0xbf, 0x23, 0xcb, 0x68, 0x00,
0xfc, 0xec, 0x9e, 0x2e, 0x2a, 0xeb, 0xc8, 0xfc, 0xfb, 0xec, 0x96, 0x2f, 0xd7, 0xef, 0x96, 0x2f, 0xd5, 0x6e, 0xb9,
0x9e, 0x63, 0xbf, 0x98, 0x74, 0xf0, 0x4f, 0xaf, 0x42, 0x08, 0x56, 0x05, 0xc8, 0x61, 0xa1, 0x5d, 0xdc, 0xea, 0xc2,
0x7f, 0x34, 0x74, 0xdb, 0xc3, 0x3f, 0x3e, 0x58, 0x80, 0x6d, 0x0b, 0x0b, 0xf1, 0x5f, 0xbb, 0x56, 0xd5, 0xb9, 0x8f,
0x75, 0xd8, 0x6b, 0x67, 0xb5, 0xae, 0x7b, 0xfd, 0xa6, 0x05, 0x79, 0xc5, 0x9d, 0x40, 0x09, 0x63, 0x70, 0xd5, 0xa2,
0xe3, 0x63, 0x28, 0x9d, 0x64, 0xa3, 0x45, 0xf1, 0x77, 0x12, 0x7e, 0x49, 0xc4, 0x6b, 0xb7, 0x74, 0x63, 0x1c, 0xd5,
0x55, 0x64, 0xbe, 0xa8, 0x11, 0x96, 0x7a, 0x9d, 0x82, 0x02, 0x18, 0x93, 0x39, 0x5d, 0xff, 0xfe, 0x9a, 0x4d, 0xf0,
0x1f, 0xb2, 0x36, 0x6b, 0x91, 0xf9, 0xb7, 0x12, 0xe3, 0x5a, 0x22, 0x7c, 0x16, 0x0d, 0xcc, 0x35, 0x6c, 0x3f, 0x5a,
0x0f, 0xee, 0xa1, 0x9a, 0x69, 0xa8, 0x94, 0x82, 0xd4, 0x3b, 0xe0, 0x05, 0x44, 0x8b, 0x84, 0x5f, 0x3f, 0xea, 0x55,
0x9c, 0xb1, 0x32, 0xea, 0x35, 0x02, 0xbd, 0x6a, 0x7b, 0x4b, 0x29, 0xfd, 0xc5, 0x97, 0xf7, 0xf1, 0x8f, 0x88, 0x7d,
0x1d, 0x57, 0xbe, 0x91, 0x88, 0x0d, 0xa0, 0x6f, 0xb4, 0x51, 0x73, 0x7e, 0x84, 0x06, 0x27, 0xff, 0xe7, 0xb6, 0xad,
0xd1, 0x58, 0xbf, 0x55, 0x73, 0x69, 0x95, 0x7e, 0x56, 0xeb, 0xcf, 0x1b, 0xfc, 0x96, 0x6d, 0x47, 0xc2, 0x21, 0xa8,
0xb7, 0x95, 0xbf, 0xb5, 0x64, 0xa5, 0xb1, 0xa2, 0xf8, 0x6d, 0xdb, 0x57, 0x26, 0x31, 0xf5, 0xd8, 0x08, 0x8f, 0xb5,
0x13, 0x29, 0xcf, 0x9d, 0xb1, 0x87, 0xf0, 0x23, 0xff, 0xc2, 0xc2, 0x7b, 0xf8, 0xc1, 0x2f, 0xeb, 0x7c, 0x96, 0xa4,
0x60, 0x56, 0x4d, 0x39, 0x9f, 0x07, 0x5b, 0x5b, 0x67, 0x67, 0x67, 0xfe, 0xd9, 0xb6, 0x9f, 0xe5, 0x27, 0x5b, 0xdd,
0x76, 0xbb, 0x8d, 0xdf, 0x69, 0xb2, 0xad, 0xd3, 0x98, 0x9d, 0x3d, 0x06, 0xf7, 0xc3, 0x7e, 0x68, 0x3d, 0xb2, 0x1e,
0x6e, 0x5b, 0x3b, 0x0f, 0x6c, 0x8b, 0x14, 0x00, 0x94, 0x6c, 0xdb, 0x96, 0x50, 0x00, 0xa1, 0x0d, 0xc5, 0xfd, 0xdd,
0x13, 0x65, 0xc3, 0x61, 0x1e, 0xbc, 0xb0, 0x90, 0xc0, 0x7f, 0xcb, 0x3e, 0xb1, 0xfa, 0x56, 0x17, 0x65, 0x2d, 0xa9,
0x46, 0xd4, 0x2b, 0xee, 0xf7, 0x51, 0x34, 0x0f, 0x88, 0x8d, 0xcc, 0x42, 0x0c, 0x93, 0x89, 0x52, 0x9a, 0x02, 0xed,
0xd2, 0x63, 0x78, 0xc2, 0xcc, 0x29, 0x0b, 0x9e, 0x5f, 0x75, 0x1f, 0x82, 0x8e, 0x3b, 0x6d, 0xdd, 0x1f, 0xb5, 0x5b,
0x1d, 0xab, 0xd3, 0xea, 0xfa, 0x0f, 0xad, 0xae, 0xf8, 0x1f, 0x64, 0xe4, 0xb6, 0xd5, 0x81, 0xa7, 0x6d, 0x0b, 0xde,
0x4f, 0xef, 0x8b, 0xb4, 0x88, 0xc8, 0xde, 0xea, 0xef, 0xe2, 0xaf, 0x2d, 0x02, 0xa4, 0xbe, 0xb4, 0xc5, 0x2f, 0x5a,
0xb3, 0xbf, 0x30, 0x4b, 0x3b, 0x8f, 0xd6, 0x16, 0x77, 0x1f, 0xae, 0x2d, 0xde, 0x7e, 0xb0, 0xb6, 0xf8, 0xfe, 0x4e,
0xbd, 0x78, 0xeb, 0x44, 0x54, 0x69, 0xb9, 0x10, 0xda, 0xb3, 0x08, 0x18, 0xe5, 0xdc, 0xe9, 0x00, 0x9c, 0x6d, 0xab,
0x85, 0x3f, 0x1e, 0x76, 0x5d, 0xdd, 0xeb, 0x18, 0x7b, 0x69, 0x2c, 0x1f, 0x3e, 0x02, 0x2c, 0x9f, 0x77, 0x1f, 0x8c,
0xb0, 0x1d, 0x21, 0x0a, 0xff, 0x4e, 0xb7, 0x1f, 0x8d, 0x40, 0x23, 0x58, 0xf8, 0x0f, 0xfe, 0x4c, 0x77, 0xba, 0x23,
0xf1, 0xd2, 0xc6, 0xfa, 0x0f, 0x9d, 0x87, 0x05, 0x34, 0xc5, 0x3f, 0xbf, 0x69, 0x13, 0x1a, 0x0d, 0x78, 0x73, 0xdc,
0xfb, 0x40, 0xa3, 0x47, 0xd3, 0xae, 0xff, 0xe5, 0xe9, 0x43, 0xff, 0xd1, 0xb4, 0xf3, 0xf0, 0x83, 0x78, 0x4b, 0x80,
0x82, 0x5f, 0xe2, 0xbf, 0x0f, 0xdb, 0xed, 0x69, 0xab, 0xe3, 0x3f, 0x3a, 0xdd, 0xf6, 0xb7, 0x93, 0xd6, 0x03, 0xff,
0x11, 0xfe, 0xab, 0x86, 0x9b, 0x66, 0x33, 0x66, 0x5b, 0xb8, 0xde, 0x0d, 0xbf, 0xd7, 0x9c, 0xa3, 0x7b, 0xdf, 0xda,
0xb9, 0xff, 0xfc, 0x11, 0xac, 0xd1, 0xb4, 0xd3, 0x85, 0xff, 0xaf, 0x7a, 0xfc, 0x80, 0x84, 0x97, 0x03, 0x47, 0x0c,
0x33, 0x58, 0x15, 0xe1, 0xe8, 0x9b, 0x61, 0xf7, 0xbc, 0xef, 0xaf, 0x0a, 0x80, 0x30, 0x7e, 0x73, 0x90, 0x9b, 0xdf,
0x2e, 0x02, 0x42, 0x1f, 0xb4, 0xff, 0x03, 0x23, 0x20, 0xdf, 0x37, 0x83, 0xdc, 0xe7, 0xab, 0x79, 0x7b, 0x4d, 0x67,
0xed, 0x35, 0x73, 0x0e, 0xff, 0xc2, 0x86, 0x98, 0xad, 0x0d, 0xad, 0x39, 0x37, 0xe3, 0x41, 0x19, 0x6e, 0xe4, 0x73,
0x19, 0xf5, 0x2f, 0xf8, 0x15, 0x04, 0x89, 0x6f, 0x26, 0xc8, 0xaf, 0xb7, 0xa3, 0x47, 0xfc, 0x07, 0xd3, 0xa3, 0xe0,
0x06, 0x3d, 0x6a, 0x11, 0x77, 0x8a, 0x18, 0x90, 0xa3, 0xbf, 0x4f, 0xef, 0xce, 0xf7, 0xf8, 0xb5, 0xaf, 0x2d, 0x86,
0x25, 0x85, 0x2d, 0x72, 0x05, 0xdf, 0x7d, 0xce, 0x09, 0x81, 0x48, 0x68, 0x0e, 0x6d, 0x19, 0x84, 0x99, 0xe3, 0x67,
0x71, 0xd5, 0xcb, 0xa9, 0xb8, 0x34, 0x13, 0xd2, 0x8d, 0xb7, 0x1d, 0x1d, 0xc0, 0xc1, 0x1c, 0xf3, 0x70, 0x99, 0xf1,
0x08, 0x7f, 0xef, 0x12, 0x8f, 0x79, 0x82, 0xf7, 0x71, 0xe5, 0xdd, 0x35, 0x4c, 0x89, 0xfe, 0x16, 0xd3, 0xb9, 0xd5,
0x41, 0xc1, 0x0c, 0x83, 0x06, 0xaf, 0xd8, 0x38, 0x8e, 0x1c, 0xdb, 0x99, 0xc3, 0xae, 0x85, 0x31, 0x5b, 0xb5, 0x5c,
0x6a, 0x4a, 0xa3, 0x76, 0x6d, 0xf5, 0xab, 0x7e, 0x72, 0xfc, 0x74, 0x5a, 0x78, 0x28, 0x83, 0x8c, 0xb6, 0xf4, 0x02,
0x60, 0x7c, 0x55, 0x92, 0xa3, 0xc0, 0xaf, 0x2c, 0x07, 0x5b, 0x98, 0x0e, 0x1d, 0xbf, 0x0b, 0xae, 0x04, 0x15, 0xe3,
0xa7, 0xa8, 0x7e, 0x70, 0x5a, 0xdb, 0x60, 0xda, 0x18, 0xdd, 0xf4, 0x40, 0xc3, 0x95, 0x50, 0x92, 0x08, 0x10, 0x34,
0x4a, 0x3d, 0xfd, 0xbb, 0xcf, 0xaa, 0x90, 0x51, 0xf1, 0xf8, 0xe2, 0x40, 0x5e, 0x37, 0xb7, 0x31, 0x7a, 0x4b, 0x51,
0xfb, 0xea, 0x53, 0x57, 0x9b, 0xa0, 0x32, 0xe8, 0x17, 0x50, 0xd2, 0x19, 0x38, 0x6a, 0x05, 0xcc, 0xf6, 0xb6, 0xa4,
0xf7, 0x10, 0xda, 0x42, 0x27, 0x8c, 0xd9, 0x69, 0x3c, 0x92, 0xa2, 0xdd, 0xb3, 0xe4, 0x2d, 0x95, 0x16, 0x61, 0x11,
0x76, 0x3c, 0xe1, 0x3f, 0xc3, 0x0b, 0x6a, 0xb6, 0x30, 0xcd, 0xec, 0xfe, 0xbd, 0x9e, 0x86, 0xa4, 0x9e, 0x28, 0x6f,
0xe3, 0x6f, 0xc3, 0x3c, 0x04, 0x7f, 0xed, 0xef, 0xc2, 0x7b, 0xf8, 0xfb, 0x30, 0xef, 0x0d, 0x6d, 0xd7, 0x27, 0xc1,
0x78, 0xaf, 0xfa, 0xa5, 0x98, 0x28, 0x15, 0x36, 0x41, 0x87, 0x79, 0xb7, 0x55, 0x66, 0x52, 0x71, 0x75, 0x77, 0x2a,
0xc5, 0x05, 0xcf, 0x86, 0xa4, 0x02, 0x21, 0xda, 0xf5, 0x77, 0x0c, 0x71, 0x78, 0xda, 0xc2, 0x9f, 0x35, 0x81, 0x78,
0x1f, 0x1a, 0x28, 0x89, 0xf8, 0x12, 0x9a, 0x6f, 0x0b, 0xe1, 0x0b, 0xfd, 0x7e, 0x24, 0x71, 0x25, 0x44, 0x55, 0x9d,
0x63, 0xd6, 0x1c, 0x24, 0x89, 0x7c, 0x01, 0xdb, 0x33, 0x62, 0x4e, 0x82, 0x5d, 0x65, 0x44, 0xe5, 0x29, 0xf4, 0x75,
0xf4, 0x17, 0xa4, 0xd7, 0xd5, 0x79, 0xb5, 0xdd, 0xb3, 0x66, 0x0a, 0x64, 0xf8, 0xc6, 0x61, 0x15, 0xdd, 0x9a, 0x10,
0x5f, 0x52, 0x13, 0x5b, 0xb9, 0xfa, 0xd6, 0xdc, 0x9a, 0xec, 0x5c, 0x73, 0x53, 0xb0, 0x8a, 0x69, 0x68, 0x5f, 0x60,
0xfa, 0x0c, 0xfe, 0xac, 0x8a, 0xd5, 0x83, 0x64, 0x28, 0x3f, 0x89, 0xf0, 0x17, 0xc1, 0xd0, 0x8f, 0xb2, 0xda, 0x80,
0x9c, 0x3e, 0x95, 0x49, 0x90, 0xbe, 0x18, 0x97, 0x4d, 0x24, 0xc0, 0x66, 0xc0, 0xdf, 0x2b, 0x58, 0xdd, 0xd2, 0x90,
0xd7, 0x2f, 0x31, 0xb5, 0x60, 0x1c, 0xe7, 0x74, 0xab, 0x57, 0xe1, 0x5f, 0x8b, 0x6a, 0x56, 0xa4, 0xa6, 0x5d, 0xc9,
0x8a, 0x81, 0x8d, 0x45, 0x76, 0x20, 0x13, 0xd3, 0xcc, 0x6f, 0x45, 0x9b, 0xd7, 0x2e, 0xc7, 0x22, 0x27, 0x0d, 0xbf,
0xa5, 0x6f, 0x0b, 0x22, 0xdb, 0x20, 0xca, 0xae, 0xc4, 0x89, 0x0c, 0x1c, 0xbc, 0xad, 0x58, 0xfd, 0x72, 0x24, 0x73,
0xc3, 0xdb, 0xe6, 0x6a, 0xe9, 0x71, 0x69, 0x1d, 0x5c, 0x19, 0xc3, 0x3b, 0x66, 0x11, 0xf7, 0xa3, 0x94, 0xf2, 0x94,
0x1c, 0x43, 0x2c, 0x78, 0x1d, 0xb6, 0xed, 0x96, 0x20, 0x79, 0x8c, 0xdf, 0x1e, 0x25, 0x48, 0xef, 0x43, 0xa1, 0xca,
0x71, 0xfe, 0xbe, 0x96, 0xa2, 0x3b, 0xed, 0xf6, 0xdf, 0x1c, 0xec, 0x59, 0x62, 0x63, 0xef, 0x6e, 0xc1, 0xeb, 0x2e,
0x79, 0xc7, 0x22, 0x63, 0x23, 0x14, 0x19, 0x1b, 0x96, 0xc8, 0xf3, 0x12, 0x29, 0xad, 0x5b, 0x02, 0x6b, 0xdb, 0x62,
0xe9, 0x48, 0x84, 0xf5, 0x66, 0xe0, 0x41, 0xc4, 0xf8, 0x79, 0xb1, 0x2d, 0xec, 0xda, 0xc2, 0x85, 0xb7, 0x55, 0xf2,
0x8b, 0xb2, 0x19, 0x78, 0xaa, 0x82, 0x80, 0xa0, 0xe9, 0x99, 0xca, 0x83, 0x91, 0x43, 0xe9, 0x74, 0xd5, 0xd5, 0xd6,
0xc5, 0xe2, 0x78, 0x06, 0x62, 0x49, 0xe5, 0xae, 0xbc, 0x97, 0x1d, 0x76, 0x69, 0xaa, 0xfe, 0x51, 0xb9, 0x2e, 0x4a,
0x39, 0xed, 0xf4, 0x77, 0x23, 0x69, 0x03, 0xe1, 0x5e, 0x2e, 0x60, 0x33, 0x83, 0xea, 0x43, 0x43, 0xc3, 0x8f, 0xb3,
0xad, 0x33, 0x76, 0xdc, 0x8a, 0xe6, 0x71, 0x15, 0x12, 0x44, 0x8d, 0xd8, 0xdf, 0x55, 0xca, 0x51, 0xa6, 0x60, 0xca,
0xc7, 0xc8, 0x48, 0xee, 0x40, 0x42, 0x12, 0xc3, 0x96, 0x32, 0xde, 0x48, 0x86, 0x24, 0x2c, 0x06, 0x00, 0x4b, 0xfc,
0xac, 0xe2, 0x92, 0x52, 0x33, 0x94, 0x76, 0xff, 0xaf, 0xff, 0xfb, 0xff, 0xc8, 0x50, 0x23, 0xd0, 0x16, 0xc0, 0xc2,
0xec, 0x98, 0xea, 0xd4, 0x91, 0x9d, 0x83, 0x73, 0x1a, 0x8f, 0x5b, 0xd3, 0x28, 0x99, 0x00, 0x04, 0x05, 0x13, 0xb9,
0xfe, 0xb2, 0x1e, 0xb8, 0x42, 0x82, 0x65, 0x9e, 0xd8, 0x4b, 0xf0, 0xea, 0x45, 0xb8, 0x6c, 0xbf, 0x2b, 0xa7, 0x55,
0xe5, 0x12, 0x13, 0x83, 0x1b, 0x19, 0xae, 0x06, 0x0f, 0xd6, 0xb2, 0x5c, 0xd5, 0xef, 0x6a, 0x92, 0xc2, 0x84, 0xd5,
0x52, 0x5c, 0xa1, 0xa5, 0x3e, 0x1c, 0xf9, 0xd7, 0x7f, 0xfa, 0x2f, 0xff, 0x43, 0xbd, 0xe2, 0x99, 0xc7, 0x5f, 0xff,
0xf1, 0xef, 0xff, 0xdf, 0xff, 0xfd, 0xaf, 0x98, 0x41, 0x2c, 0xcf, 0x45, 0x68, 0x6b, 0x59, 0xd5, 0xa1, 0x88, 0xd8,
0x63, 0x56, 0xe5, 0x84, 0xd4, 0x53, 0x61, 0xf7, 0x69, 0x42, 0x62, 0x50, 0x09, 0x1d, 0xf1, 0x39, 0xa5, 0x4e, 0x13,
0xd5, 0xae, 0x21, 0x1f, 0x2c, 0xa5, 0x45, 0x47, 0xfd, 0xf6, 0x4e, 0xdb, 0xae, 0x96, 0xb7, 0x6f, 0xf4, 0xdd, 0xc2,
0x85, 0xb9, 0x55, 0x62, 0x8e, 0xaf, 0x97, 0x6d, 0xa9, 0x42, 0x5b, 0x58, 0x52, 0x56, 0xe5, 0x16, 0xc6, 0x9c, 0x97,
0xf8, 0x1a, 0x74, 0x8d, 0x62, 0x5a, 0xe5, 0x5a, 0x9f, 0xde, 0x2f, 0x0b, 0x40, 0x74, 0x82, 0x4b, 0x23, 0x02, 0x68,
0x74, 0x9e, 0xda, 0x42, 0x6b, 0x25, 0xb9, 0x28, 0x69, 0x14, 0xe1, 0xe1, 0xdc, 0x7f, 0xf4, 0xb7, 0xe5, 0x9f, 0x67,
0x68, 0x25, 0x58, 0xce, 0x2c, 0x3a, 0x97, 0x7e, 0xcf, 0x83, 0x76, 0x7b, 0x7e, 0xee, 0x2e, 0xab, 0x19, 0xbc, 0xab,
0x26, 0xa3, 0xa0, 0x9b, 0x39, 0x20, 0x1d, 0xc4, 0xea, 0xf8, 0x1e, 0x98, 0xfa, 0x6d, 0x0c, 0x07, 0x95, 0xe5, 0x9f,
0x96, 0x14, 0x62, 0x8a, 0x7f, 0xc3, 0x03, 0x53, 0x19, 0x8d, 0x83, 0x12, 0x03, 0x8b, 0xa5, 0xd1, 0xab, 0x2b, 0x7a,
0x4d, 0x3b, 0xab, 0x29, 0x2b, 0xe6, 0x81, 0xaf, 0x79, 0x54, 0x7b, 0x1f, 0x0f, 0x5f, 0xa7, 0x1d, 0x6f, 0xda, 0x5d,
0xea, 0xe1, 0x39, 0xcf, 0x66, 0xe6, 0x09, 0x2f, 0x8b, 0xd8, 0x88, 0x4d, 0x54, 0x14, 0x53, 0xd6, 0x8b, 0xd3, 0xdb,
0xf2, 0x0b, 0xdc, 0x6e, 0x40, 0xdb, 0x2c, 0xe2, 0x01, 0x31, 0x6d, 0xcf, 0x3c, 0x03, 0x8e, 0xf0, 0x74, 0x3d, 0x5b,
0x1a, 0x73, 0xf5, 0x44, 0x53, 0x8c, 0x15, 0xd6, 0xd3, 0x81, 0x4a, 0x9f, 0xba, 0x9b, 0x43, 0x89, 0x50, 0xc3, 0xaf,
0xf2, 0x68, 0xf5, 0x5d, 0xcd, 0x47, 0x97, 0xa2, 0x19, 0xdc, 0xe2, 0xb5, 0xf5, 0x42, 0x4d, 0x8a, 0xda, 0x0f, 0x60,
0xfd, 0x10, 0x98, 0x76, 0xb3, 0x15, 0x15, 0x62, 0xab, 0x77, 0xe1, 0xaf, 0xda, 0x1e, 0x8f, 0xe6, 0x73, 0x6a, 0xe8,
0x02, 0x37, 0x92, 0x5d, 0x8d, 0x92, 0x82, 0xd2, 0x06, 0xc4, 0x29, 0xbd, 0x6c, 0x23, 0xd9, 0x56, 0x3c, 0xc9, 0xf3,
0x7b, 0xfa, 0xd5, 0xdf, 0xff, 0x1f, 0x2a, 0x12, 0x27, 0x08, 0x10, 0x7c, 0x00, 0x00};
} // namespace web_server
} // namespace esphome

View File

@ -267,7 +267,7 @@ CONFIG_SCHEMA = cv.All(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.SplitDefault(
CONF_POWER_SAVE_MODE, esp8266="none", esp32="light"
CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", rp2040="light"
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
@ -368,7 +368,7 @@ async def to_code(config):
if CONF_AP in config:
conf = config[CONF_AP]
ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
ip_config = conf.get(CONF_MANUAL_IP)
cg.with_local_variable(
conf[CONF_ID],
WiFiAP(),
@ -386,6 +386,8 @@ async def to_code(config):
cg.add_library("ESP8266WiFi", None)
elif CORE.is_esp32 and CORE.using_arduino:
cg.add_library("WiFi", None)
elif CORE.is_rp2040:
cg.add_library("WiFi", None)
if CORE.is_esp32 and CORE.using_esp_idf:
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:

View File

@ -35,10 +35,12 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up WiFi...");
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
this->last_connected_ = millis();
this->wifi_pre_setup_();
uint32_t hash = fnv1_hash(App.get_compilation_time());
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
SavedWifiSettings save{};
@ -706,6 +708,8 @@ int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace wifi

View File

@ -24,6 +24,16 @@ extern "C" {
#endif
#endif
#ifdef USE_RP2040
extern "C" {
#include "cyw43.h"
#include "cyw43_country.h"
#include "pico/cyw43_arch.h"
}
#include <WiFi.h>
#endif
namespace esphome {
namespace wifi {
@ -138,6 +148,8 @@ class WiFiScanResult {
float get_priority() const { return priority_; }
void set_priority(float priority) { priority_ = priority; }
bool operator==(const WiFiScanResult &rhs) const;
protected:
bool matches_{false};
bssid_t bssid_;
@ -310,6 +322,11 @@ class WiFiComponent : public Component {
void wifi_process_event_(IDFWiFiEvent *data);
#endif
#ifdef USE_RP2040
static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result);
void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result);
#endif
std::string use_address_;
std::vector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;

View File

@ -0,0 +1,194 @@
#include "wifi_component.h"
#ifdef USE_RP2040
#include "lwip/dns.h"
#include "lwip/err.h"
#include "lwip/netif.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
static const char *const TAG = "wifi_pico_w";
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
if (sta.has_value()) {
if (sta.value()) {
cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE);
}
}
if (ap.has_value()) {
if (ap.value()) {
cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE);
}
}
return true;
}
bool WiFiComponent::wifi_apply_power_save_() {
uint32_t pm;
switch (this->power_save_) {
case WIFI_POWER_SAVE_NONE:
pm = CYW43_PERFORMANCE_PM;
break;
case WIFI_POWER_SAVE_LIGHT:
pm = CYW43_DEFAULT_PM;
break;
case WIFI_POWER_SAVE_HIGH:
pm = CYW43_AGGRESSIVE_PM;
break;
}
int ret = cyw43_wifi_pm(&cyw43_state, pm);
return ret == 0;
}
// TODO: The driver doesnt seem to have an API for this
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
if (!this->wifi_sta_ip_config_(ap.get_manual_ip()))
return false;
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
if (ret != WL_CONNECTED)
return false;
return true;
}
bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
if (!manual_ip.has_value()) {
return true;
}
IPAddress ip_address = IPAddress(manual_ip->static_ip);
IPAddress gateway = IPAddress(manual_ip->gateway);
IPAddress subnet = IPAddress(manual_ip->subnet);
IPAddress dns = IPAddress(manual_ip->dns1);
WiFi.config(ip_address, dns, gateway, subnet);
return true;
}
bool WiFiComponent::wifi_apply_hostname_() {
WiFi.setHostname(App.get_name().c_str());
return true;
}
const char *get_auth_mode_str(uint8_t mode) {
// TODO:
return "UNKNOWN";
}
const char *get_disconnect_reason_str(uint8_t reason) {
// TODO:
return "UNKNOWN";
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
switch (status) {
case CYW43_LINK_JOIN:
case CYW43_LINK_NOIP:
return WiFiSTAConnectStatus::CONNECTING;
case CYW43_LINK_UP:
return WiFiSTAConnectStatus::CONNECTED;
case CYW43_LINK_FAIL:
case CYW43_LINK_BADAUTH:
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
case CYW43_LINK_NONET:
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
}
return WiFiSTAConnectStatus::IDLE;
}
int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
global_wifi_component->wifi_scan_result(env, result);
return 0;
}
void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(result->ssid));
WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty());
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res);
}
}
bool WiFiComponent::wifi_scan_start_() {
this->scan_result_.clear();
this->scan_done_ = false;
cyw43_wifi_scan_options_t scan_options = {0};
int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result);
if (err) {
ESP_LOGV(TAG, "cyw43_wifi_scan failed!");
}
return err == 0;
return true;
}
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// TODO:
return false;
}
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
if (!this->wifi_mode_({}, true))
return false;
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.localIP()}; }
bool WiFiComponent::wifi_disconnect_() {
int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA);
return err == 0;
}
// NOTE: The driver does not provide an interface to get this
bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{};
uint8_t raw_bssid[6];
WiFi.BSSID(raw_bssid);
for (size_t i = 0; i < bssid.size(); i++)
bssid[i] = raw_bssid[i];
return bssid;
}
// NOTE: The driver does not provide an interface to get this
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID(); }
// NOTE: The driver does not provide an interface to get this
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
// NOTE: The driver does not provide an interface to get this
int32_t WiFiComponent::wifi_channel_() { return 0; }
network::IPAddress WiFiComponent::wifi_sta_ip() { return {WiFi.localIP()}; }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
const ip_addr_t *dns_ip = dns_getserver(num);
return {dns_ip->addr};
}
void WiFiComponent::wifi_loop_() {
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true;
ESP_LOGV(TAG, "Scan done!");
}
}
void WiFiComponent::wifi_pre_setup_() {}
} // namespace wifi
} // namespace esphome
#endif

View File

View File

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor, uart
from esphome.const import (
ICON_FINGERPRINT,
)
CODEOWNERS = ["@hobbypunk90"]
DEPENDENCIES = ["uart"]
CONF_RESET = "reset"
wl134_ns = cg.esphome_ns.namespace("wl_134")
Wl134Component = wl134_ns.class_(
"Wl134Component", text_sensor.TextSensor, cg.Component, uart.UARTDevice
)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(
Wl134Component,
icon=ICON_FINGERPRINT,
)
.extend({cv.Optional(CONF_RESET, default=False): cv.boolean})
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config):
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_do_reset(config[CONF_RESET]))
await uart.register_uart_device(var, config)

View File

@ -0,0 +1,111 @@
#include "wl_134.h"
#include "esphome/core/log.h"
namespace esphome {
namespace wl_134 {
static const char *const TAG = "wl_134.sensor";
static const uint8_t ASCII_CR = 0x0D;
static const uint8_t ASCII_NBSP = 0xFF;
static const int MAX_DATA_LENGTH_BYTES = 6;
void Wl134Component::setup() { this->publish_state(""); }
void Wl134Component::loop() {
while (this->available() >= RFID134_PACKET_SIZE) {
Wl134Component::Rfid134Error error = this->read_packet_();
if (error != RFID134_ERROR_NONE) {
ESP_LOGW(TAG, "Error: %d", error);
}
}
}
Wl134Component::Rfid134Error Wl134Component::read_packet_() {
uint8_t packet[RFID134_PACKET_SIZE];
packet[RFID134_PACKET_START_CODE] = this->read();
// check for the first byte being the packet start code
if (packet[RFID134_PACKET_START_CODE] != 0x02) {
// just out of sync, ignore until we are synced up
return RFID134_ERROR_NONE;
}
if (!this->read_array(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_SIZE - 1)) {
return RFID134_ERROR_PACKET_SIZE;
}
if (packet[RFID134_PACKET_END_CODE] != 0x03) {
return RFID134_ERROR_PACKET_END_CODE_MISSMATCH;
}
// calculate checksum
uint8_t checksum = 0;
for (uint8_t i = RFID134_PACKET_ID; i < RFID134_PACKET_CHECKSUM; i++) {
checksum = checksum ^ packet[i];
}
// test checksum
if (checksum != packet[RFID134_PACKET_CHECKSUM]) {
return RFID134_ERROR_PACKET_CHECKSUM;
}
if (static_cast<uint8_t>(~checksum) != static_cast<uint8_t>(packet[RFID134_PACKET_CHECKSUM_INVERT])) {
return RFID134_ERROR_PACKET_CHECKSUM_INVERT;
}
Rfid134Reading reading;
// convert packet into the reading struct
reading.id = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_COUNTRY - RFID134_PACKET_ID);
reading.country = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_COUNTRY]),
RFID134_PACKET_DATA_FLAG - RFID134_PACKET_COUNTRY);
reading.isData = packet[RFID134_PACKET_DATA_FLAG] == '1';
reading.isAnimal = packet[RFID134_PACKET_ANIMAL_FLAG] == '1';
reading.reserved0 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED0]),
RFID134_PACKET_RESERVED1 - RFID134_PACKET_RESERVED0);
reading.reserved1 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED1]),
RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1);
ESP_LOGV(TAG, "Tag id: %012lld", reading.id);
ESP_LOGV(TAG, "Country: %03d", reading.country);
ESP_LOGV(TAG, "isData: %s", reading.isData ? "true" : "false");
ESP_LOGV(TAG, "isAnimal: %s", reading.isAnimal ? "true" : "false");
ESP_LOGV(TAG, "Reserved0: %d", reading.reserved0);
ESP_LOGV(TAG, "Reserved1: %d", reading.reserved1);
char buf[20];
sprintf(buf, "%03d%012lld", reading.country, reading.id);
this->publish_state(buf);
if (this->do_reset_) {
this->set_timeout(1000, [this]() { this->publish_state(""); });
}
return RFID134_ERROR_NONE;
}
uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size) {
uint64_t value = 0;
uint8_t i = text_size;
do {
i--;
uint8_t digit = text[i];
if (digit >= 'A') {
digit = digit - 'A' + 10;
} else {
digit = digit - '0';
}
value = (value << 4) + digit;
} while (i != 0);
return value;
}
void Wl134Component::dump_config() {
ESP_LOGCONFIG(TAG, "WL-134 Sensor:");
LOG_TEXT_SENSOR("", "Tag", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
}
} // namespace wl_134
} // namespace esphome

View File

@ -0,0 +1,63 @@
#pragma once
#include <string>
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace wl_134 {
class Wl134Component : public text_sensor::TextSensor, public Component, public uart::UARTDevice {
public:
enum Rfid134Error {
RFID134_ERROR_NONE,
// from library
RFID134_ERROR_PACKET_SIZE = 0x81,
RFID134_ERROR_PACKET_END_CODE_MISSMATCH,
RFID134_ERROR_PACKET_CHECKSUM,
RFID134_ERROR_PACKET_CHECKSUM_INVERT
};
struct Rfid134Reading {
uint16_t country;
uint64_t id;
bool isData;
bool isAnimal;
uint16_t reserved0;
uint32_t reserved1;
};
// Nothing really public.
// ========== INTERNAL METHODS ==========
void setup() override;
void loop() override;
void dump_config() override;
void set_do_reset(bool do_reset) { this->do_reset_ = do_reset; }
private:
enum DfMp3Packet {
RFID134_PACKET_START_CODE,
RFID134_PACKET_ID = 1,
RFID134_PACKET_COUNTRY = 11,
RFID134_PACKET_DATA_FLAG = 15,
RFID134_PACKET_ANIMAL_FLAG = 16,
RFID134_PACKET_RESERVED0 = 17,
RFID134_PACKET_RESERVED1 = 21,
RFID134_PACKET_CHECKSUM = 27,
RFID134_PACKET_CHECKSUM_INVERT = 28,
RFID134_PACKET_END_CODE = 29,
RFID134_PACKET_SIZE
};
bool do_reset_;
Rfid134Error read_packet_();
uint64_t hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size);
};
} // namespace wl_134
} // namespace esphome

View File

@ -44,6 +44,7 @@ from esphome.const import (
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
)
from esphome.core import (
CORE,
@ -1440,6 +1441,7 @@ class SplitDefault(Optional):
esp32=vol.UNDEFINED,
esp32_arduino=vol.UNDEFINED,
esp32_idf=vol.UNDEFINED,
rp2040=vol.UNDEFINED,
):
super().__init__(key)
self._esp8266_default = vol.default_factory(esp8266)
@ -1449,6 +1451,7 @@ class SplitDefault(Optional):
self._esp32_idf_default = vol.default_factory(
esp32_idf if esp32 is vol.UNDEFINED else esp32
)
self._rp2040_default = vol.default_factory(rp2040)
@property
def default(self):
@ -1458,6 +1461,8 @@ class SplitDefault(Optional):
return self._esp32_arduino_default
if CORE.is_esp32 and CORE.using_esp_idf:
return self._esp32_idf_default
if CORE.is_rp2040:
return self._rp2040_default
raise NotImplementedError
@default.setter
@ -1730,6 +1735,7 @@ def require_framework_version(
esp_idf=None,
esp32_arduino=None,
esp8266_arduino=None,
rp2040_arduino=None,
max_version=False,
extra_message=None,
):
@ -1757,8 +1763,23 @@ def require_framework_version(
msg += f". {extra_message}"
raise Invalid(msg)
required = esp8266_arduino
elif CORE.is_rp2040 and framework == "arduino":
if rp2040_arduino is None:
msg = "This feature is incompatible with RP2040"
if extra_message:
msg += f". {extra_message}"
raise Invalid(msg)
required = rp2040_arduino
else:
raise NotImplementedError
raise Invalid(
f"""
Internal Error: require_framework_version does not support this platform configuration
platform: {core_data[KEY_TARGET_PLATFORM]}
framework: {framework}
Please report this issue on GitHub -> https://github.com/esphome/issues/issues/new?template=bug_report.yml.
"""
)
if max_version:
if core_data[KEY_FRAMEWORK_VERSION] > required:

View File

@ -1,13 +1,14 @@
"""Constants used by esphome."""
__version__ = "2022.10.2"
__version__ = "2022.11.0b6"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
PLATFORM_ESP32 = "esp32"
PLATFORM_ESP8266 = "esp8266"
PLATFORM_RP2040 = "rp2040"
TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266]
TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
@ -115,6 +116,7 @@ CONF_COMMAND_RETAIN = "command_retain"
CONF_COMMAND_TOPIC = "command_topic"
CONF_COMMENT = "comment"
CONF_COMMIT = "commit"
CONF_COMPILE_PROCESS_LIMIT = "compile_process_limit"
CONF_COMPONENT_ID = "component_id"
CONF_COMPONENTS = "components"
CONF_CONDITION = "condition"
@ -489,6 +491,7 @@ CONF_PACKAGES = "packages"
CONF_PAGE_ID = "page_id"
CONF_PAGES = "pages"
CONF_PANASONIC = "panasonic"
CONF_PARAMETERS = "parameters"
CONF_PASSWORD = "password"
CONF_PATH = "path"
CONF_PAYLOAD = "payload"

View File

@ -594,6 +594,10 @@ class EsphomeCore:
def is_esp32(self):
return self.target_platform == "esp32"
@property
def is_rp2040(self):
return self.target_platform == "rp2040"
@property
def target_framework(self):
return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK]

View File

@ -20,6 +20,7 @@ const float PROCESSOR = 400.0;
const float BLUETOOTH = 350.0f;
const float AFTER_BLUETOOTH = 300.0f;
const float WIFI = 250.0f;
const float ETHERNET = 250.0f;
const float BEFORE_CONNECTION = 220.0f;
const float AFTER_WIFI = 200.0f;
const float AFTER_CONNECTION = 100.0f;

View File

@ -29,6 +29,7 @@ extern const float PROCESSOR;
extern const float BLUETOOTH;
extern const float AFTER_BLUETOOTH;
extern const float WIFI;
extern const float ETHERNET;
/// For components that should be initialized after WiFi and before API is connected.
extern const float BEFORE_CONNECTION;
/// For components that should be initialized after WiFi is connected.

View File

@ -1,4 +1,5 @@
import logging
import multiprocessing
import os
import re
@ -11,6 +12,7 @@ from esphome.const import (
CONF_BOARD_FLASH_MODE,
CONF_BUILD_PATH,
CONF_COMMENT,
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
CONF_FRAMEWORK,
CONF_INCLUDES,
@ -151,6 +153,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All(
cv.version_number, validate_version
),
cv.Optional(CONF_COMPILE_PROCESS_LIMIT): cv.int_range(
min=1, max=multiprocessing.cpu_count()
),
}
),
validate_hostname,

View File

@ -21,6 +21,12 @@
#include "esp_system.h"
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
#elif defined(USE_RP2040)
#if defined(USE_WIFI)
#include <WiFi.h>
#endif
#include <hardware/structs/rosc.h>
#include <hardware/sync.h>
#endif
#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC
@ -91,6 +97,13 @@ uint32_t random_uint32() {
return esp_random();
#elif defined(USE_ESP8266)
return os_random();
#elif defined(USE_RP2040)
uint32_t result = 0;
for (uint8_t i = 0; i < 32; i++) {
result <<= 1;
result |= rosc_hw->randombit;
}
return result;
#else
#error "No random source available for this configuration."
#endif
@ -102,6 +115,16 @@ bool random_bytes(uint8_t *data, size_t len) {
return true;
#elif defined(USE_ESP8266)
return os_get_random(data, len) == 0;
#elif defined(USE_RP2040)
while (len-- != 0) {
uint8_t result = 0;
for (uint8_t i = 0; i < 8; i++) {
result <<= 1;
result |= rosc_hw->randombit;
}
*data++ = result;
}
return true;
#else
#error "No random source available for this configuration."
#endif
@ -372,13 +395,16 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
// System APIs
#if defined(USE_ESP8266)
IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); }
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
#elif defined(USE_ESP32)
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
#elif defined(USE_RP2040)
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
#endif
uint8_t HighFrequencyLoopRequester::num_requests = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@ -409,6 +435,8 @@ void get_mac_address_raw(uint8_t *mac) {
#endif
#elif defined(USE_ESP8266)
wifi_get_macaddr(STATION_IF, mac);
#elif defined(USE_RP2040) && defined(USE_WIFI)
WiFi.macAddress(mac);
#endif
}
std::string get_mac_address() {

View File

@ -542,8 +542,8 @@ class InterruptLock {
~InterruptLock();
protected:
#ifdef USE_ESP8266
uint32_t xt_state_;
#if defined(USE_ESP8266) || defined(USE_RP2040)
uint32_t state_;
#endif
};

View File

@ -29,6 +29,7 @@ import tornado.web
import tornado.websocket
from esphome import const, platformio_api, util, yaml_util
from esphome.core import EsphomeError
from esphome.helpers import mkdir_p, get_bool_env, run_system_command
from esphome.storage_json import (
EsphomeStorageJSON,
@ -312,7 +313,11 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket):
class EsphomeCompileHandler(EsphomeCommandWebSocket):
def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"])
return ["esphome", "--dashboard", "compile", config_file]
command = ["esphome", "--dashboard", "compile"]
if json_message.get("only_generate", False):
command.append("--only-generate")
command.append(config_file)
return command
class EsphomeValidateHandler(EsphomeCommandWebSocket):
@ -427,21 +432,27 @@ class DownloadBinaryRequestHandler(BaseHandler):
def get(self, configuration=None):
type = self.get_argument("type", "firmware.bin")
if type == "firmware.bin":
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_json = StorageJSON.load(storage_path)
if storage_json is None:
self.send_error(404)
return
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_json = StorageJSON.load(storage_path)
if storage_json is None:
self.send_error(404)
return
if storage_json.target_platform.lower() == const.PLATFORM_RP2040:
filename = f"{storage_json.name}.uf2"
path = storage_json.firmware_bin_path.replace(
"firmware.bin", "firmware.uf2"
)
elif storage_json.target_platform.lower() == const.PLATFORM_ESP8266:
filename = f"{storage_json.name}.bin"
path = storage_json.firmware_bin_path
elif type == "firmware.bin":
filename = f"{storage_json.name}.bin"
path = storage_json.firmware_bin_path
elif type == "firmware-factory.bin":
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_json = StorageJSON.load(storage_path)
if storage_json is None:
self.send_error(404)
return
filename = f"{storage_json.name}-factory.bin"
path = storage_json.firmware_bin_path.replace(
"firmware.bin", "firmware-factory.bin"
@ -927,6 +938,27 @@ class SecretKeysRequestHandler(BaseHandler):
self.write(json.dumps(secret_keys))
class JsonConfigRequestHandler(BaseHandler):
@authenticated
@bind_config
def get(self, configuration=None):
filename = settings.rel_path(configuration)
if not os.path.isfile(filename):
self.send_error(404)
return
try:
content = yaml_util.load_yaml(filename, clear_secrets=False)
json_content = json.dumps(
content, default=lambda o: {"__type": str(type(o)), "repr": repr(o)}
)
self.set_header("content-type", "application/json")
self.write(json_content)
except EsphomeError as err:
_LOGGER.warning("Error translating file %s to JSON: %s", filename, err)
self.send_error(500)
def get_base_frontend_path():
if ENV_DEV not in os.environ:
import esphome_dashboard
@ -1031,6 +1063,7 @@ def make_app(debug=get_bool_env(ENV_DEV)):
(f"{rel}devices", ListDevicesHandler),
(f"{rel}import", ImportRequestHandler),
(f"{rel}secret_keys", SecretKeysRequestHandler),
(f"{rel}json-config", JsonConfigRequestHandler),
(f"{rel}rename", EsphomeRenameHandler),
(f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler),
],

View File

@ -8,7 +8,7 @@ import os
import re
import subprocess
from esphome.const import KEY_CORE
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
from esphome.core import CORE, EsphomeError
from esphome.util import run_external_command, run_external_process
@ -103,7 +103,10 @@ def run_platformio_cli_run(config, verbose, *args, **kwargs) -> Union[str, int]:
def run_compile(config, verbose):
return run_platformio_cli_run(config, verbose)
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):

View File

@ -119,16 +119,16 @@ class StorageJSON:
)
@staticmethod
def from_wizard(name: str, address: str, esp_platform: str) -> "StorageJSON":
def from_wizard(name: str, address: str, platform: str) -> "StorageJSON":
return StorageJSON(
storage_version=1,
name=name,
comment=None,
esphome_version=const.__version__,
esphome_version=None,
src_version=1,
address=address,
web_port=None,
target_platform=esp_platform,
target_platform=platform,
build_path=None,
firmware_bin_path=None,
loaded_integrations=[],

View File

@ -81,11 +81,20 @@ esp32:
type: esp-idf
"""
RP2040_CONFIG = """
rp2040:
board: {board}
framework:
# Required until https://github.com/platformio/platform-raspberrypi/pull/36 is merged
platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git
"""
HARDWARE_BASE_CONFIGS = {
"ESP8266": ESP8266_CONFIG,
"ESP32": ESP32_CONFIG,
"ESP32S2": ESP32S2_CONFIG,
"ESP32C3": ESP32C3_CONFIG,
"RP2040": RP2040_CONFIG,
}
@ -164,6 +173,7 @@ captive_portal:
def wizard_write(path, **kwargs):
from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.rp2040 import boards as rp2040_boards
name = kwargs["name"]
board = kwargs["board"]
@ -173,9 +183,13 @@ def wizard_write(path, **kwargs):
kwargs[key] = sanitize_double_quotes(kwargs[key])
if "platform" not in kwargs:
kwargs["platform"] = (
"ESP8266" if board in esp8266_boards.ESP8266_BOARD_PINS else "ESP32"
)
if board in esp8266_boards.ESP8266_BOARD_PINS:
platform = "ESP8266"
elif board in rp2040_boards.RP2040_BOARD_PINS:
platform = "RP2040"
else:
platform = "ESP32"
kwargs["platform"] = platform
hardware = kwargs["platform"]
write_file(path, wizard_file(**kwargs))

View File

@ -35,7 +35,7 @@ build_flags =
lib_deps =
esphome/noise-c@0.1.4 ; api
makuna/NeoPixelBus@2.6.9 ; neopixelbus
esphome/Improv@1.2.1 ; improv_serial / esp32_improv
esphome/Improv@1.2.3 ; improv_serial / esp32_improv
bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
@ -92,6 +92,7 @@ lib_deps =
ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in)
crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir
build_flags =
${common:arduino.build_flags}
-Wno-nonnull-compare
@ -111,16 +112,17 @@ board = nodemcu-32s
lib_deps =
; order matters with lib-deps; some of the libs in common:arduino.lib_deps
; don't declare built-in libraries as dependencies, so they have to be declared first
FS ; web_server_base (Arduino built-in)
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in)
FS ; web_server_base (Arduino built-in)
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps}
esphome/AsyncTCP-esphome@1.2.2 ; async_tcp
WiFiClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.1.0 ; i2s_audio
esphome/AsyncTCP-esphome@1.2.2 ; async_tcp
WiFiClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.1.0 ; i2s_audio
crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir
build_flags =
${common:arduino.build_flags}
-DUSE_ESP32
@ -146,6 +148,25 @@ build_flags =
-DUSE_ESP32_FRAMEWORK_ESP_IDF
extra_scripts = post:esphome/components/esp32/post_build.py.script
; These are common settings for the RP2040 using Arduino.
[common:rp2040-arduino]
extends = common:arduino
board_build.core = earlephilhower
board_build.filesystem_size = 0.5m
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
platform_packages =
; earlephilhower/framework-arduinopico @ ~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted
earlephilhower/framework-arduinopico @ https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip
framework = arduino
lib_deps =
${common:arduino.lib_deps}
build_flags =
${common:arduino.build_flags}
-DUSE_RP2040
-DUSE_RP2040_FRAMEWORK_ARDUINO
; All the actual environments are defined below.
[env:esp8266-arduino]
extends = common:esp8266-arduino
@ -222,3 +243,10 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy
build_flags =
${common:esp32-idf.build_flags}
${flags:clangtidy.build_flags}
[env:rp2040-pico-arduino]
extends = common:rp2040-arduino
board = rpipico
build_flags =
${common:rp2040-arduino.build_flags}
${flags:runtime.build_flags}

View File

@ -2,16 +2,16 @@ voluptuous==0.13.1
PyYAML==6.0
paho-mqtt==1.6.1
colorama==0.4.5
tornado==6.1
tornado==6.2
tzlocal==4.2 # from time
tzdata>=2021.1 # from time
pyserial==3.5
platformio==6.0.2 # When updating platformio, also update Dockerfile
platformio==6.1.5 # When updating platformio, also update Dockerfile
esptool==3.3.1
click==8.1.3
esphome-dashboard==20221007.0
aioesphomeapi==10.13.0
zeroconf==0.39.1
esphome-dashboard==20221109.0
aioesphomeapi==11.4.3
zeroconf==0.39.4
# esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View File

@ -1,13 +1,13 @@
pylint==2.15.3
flake8==5.0.4
black==22.8.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.0.0 # also change in .pre-commit-config.yaml when updating
pylint==2.15.5
flake8==5.0.4 # also change in .pre-commit-config.yaml when updating
black==22.10.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.2.0 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==7.1.3
pytest-cov==3.0.0
pytest-mock==3.8.2
pytest-asyncio==0.19.0
pytest==7.2.0
pytest-cov==4.0.0
pytest-mock==3.10.0
pytest-asyncio==0.20.1
asyncmock==0.4.2
hypothesis==5.49.0

View File

@ -534,6 +534,7 @@ def lint_relative_py_import(fname):
"esphome/components/socket/headers.h",
"esphome/components/esp32/core.cpp",
"esphome/components/esp8266/core.cpp",
"esphome/components/rp2040/core.cpp",
],
)
def lint_namespace(fname, content):

View File

@ -24,3 +24,4 @@ Current test_.yaml file contents.
| test3.yaml | ESP8266 | wifi | N/A
| test4.yaml | ESP32 | ethernet | None
| test5.yaml | ESP32 | wifi | ble_server
| test6.yaml | RP2040 | wifi | N/A

View File

@ -13,8 +13,9 @@ using namespace esphome;
void setup() {
App.pre_setup("livingroom", __DATE__ ", " __TIME__, false);
auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); // NOLINT
auto *log = new logger::Logger(115200, 512); // NOLINT
log->pre_setup();
log->set_uart_selection(logger::UART_SELECTION_UART0);
App.register_component(log);
auto *wifi = new wifi::WiFiComponent(); // NOLINT

View File

@ -871,8 +871,10 @@ sensor:
value: !lambda "return -1;"
on_clockwise:
- logger.log: Clockwise
- display_menu.down:
on_anticlockwise:
- logger.log: Anticlockwise
- display_menu.up:
- platform: pulse_width
name: Pulse Width
pin: GPIO12
@ -1289,6 +1291,16 @@ binary_sensor:
pin: GPIO27
threshold: 1000
id: btn_left
on_press:
- if:
condition:
display_menu.is_active:
then:
- display_menu.enter:
else:
- display_menu.left:
- display_menu.right:
- display_menu.show:
- platform: template
name: Garage Door Open
id: garage_door
@ -1862,6 +1874,9 @@ climate:
name: Fujitsu General Climate
- platform: daikin
name: Daikin Climate
- platform: daikin_brc
name: Daikin BRC Climate
use_fahrenheit: true
- platform: delonghi
name: Delonghi Climate
- platform: yashima
@ -2328,6 +2343,7 @@ color:
display:
- platform: lcd_gpio
id: my_lcd_gpio
dimensions: 18x4
data_pins:
- GPIO19
@ -3006,3 +3022,85 @@ button:
name: Midea Power Inverse
on_press:
midea_ac.power_toggle:
lcd_menu:
display_id: my_lcd_gpio
mark_back: 0x5e
mark_selected: 0x3e
mark_editing: 0x2a
mark_submenu: 0x7e
active: false
mode: rotary
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "root enter");'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "root leave");'
items:
- type: back
text: 'Back'
- type: label
- type: menu
text: 'Submenu 1'
items:
- type: back
text: 'Back'
- type: menu
text: 'Submenu 21'
items:
- type: back
text: 'Back'
- type: command
text: 'Show Main'
on_value:
then:
- display_menu.show_main:
- type: select
text: 'Enum Item'
immediate_edit: true
select: test_select
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: number
text: 'Number'
number: test_number
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: command
text: 'Hide'
on_value:
then:
- display_menu.hide:
- type: switch
text: 'Switch'
switch: my_switch
on_text: 'Bright'
off_text: 'Dark'
immediate_edit: false
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());'
- type: custom
text: !lambda 'return "Custom";'
value_lambda: 'return "Val";'
on_next:
then:
lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());'
on_prev:
then:
lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());'

View File

@ -532,6 +532,16 @@ text_sensor:
ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str());
# yamllint enable rule:line-length
- script.execute: my_script
- script.execute:
id: my_script_with_params
prefix: Running my_script_with_params
param2: 100
param3: true
- script.execute:
id: my_script_with_params
prefix: Running my_script_with_params using lambda parameters
param2: !lambda return 200;
param3: !lambda return true;
- homeassistant.service:
service: notify.html5
data:
@ -597,6 +607,13 @@ script:
mode: restart
then:
- lambda: 'ESP_LOGD("main", "Hello World!");'
- id: my_script_with_params
parameters:
prefix: string
param2: int
param3: bool
then:
- lambda: 'ESP_LOGD("main", (prefix + " Hello World!" + to_string(param2) + " " + to_string(param3)).c_str());'
stepper:
- platform: uln2003

View File

@ -789,6 +789,11 @@ sensor:
voltage:
name: Voltage
update_interval: 60s
- platform: adc128s102
id: adc128s102_channel_0
channel: 0
time:
- platform: homeassistant
@ -879,6 +884,7 @@ binary_sensor:
then:
- cover.toggle: time_based_cover
- cover.toggle: endstop_cover
- cover.toggle: current_based_cover
- platform: hydreon_rgxx
hydreon_rgxx_id: hydreon_rg9
too_cold:
@ -1241,6 +1247,7 @@ cover:
close_duration: 4.5min
- platform: current_based
name: Current Based Cover
id: current_based_cover
open_sensor: ade7953_current_a
open_moving_current_threshold: 0.5
open_obstacle_current_threshold: 0.8
@ -1540,3 +1547,6 @@ cd74hc4067:
pin_s1: GPIO13
pin_s2: GPIO14
pin_s3: GPIO15
adc128s102:
cs_pin: GPIO12

View File

@ -532,6 +532,9 @@ text_sensor:
- platform: copy
source_id: inverter0_device_mode
name: Inverter Text Sensor Copy
- platform: ethernet_info
ip_address:
name: IP Address
output:
- platform: pipsolar

View File

@ -165,6 +165,12 @@ binary_sensor:
- platform: ezo_pmp
pump_state:
name: "Pump State"
is_paused:
name: "Is Paused"
tlc5947:
data_pin: GPIO12
clock_pin: GPIO14
@ -220,6 +226,10 @@ esp32_improv:
authorized_duration: 1min
status_indicator: built_in_led
ezo_pmp:
id: hcl_pump
update_interval: 1s
number:
- platform: template
name: My template number
@ -440,6 +450,20 @@ sensor:
cold_junction:
name: Ambient Temperature
- platform: ezo_pmp
current_volume_dosed:
name: Current Volume Dosed
total_volume_dosed:
name: Total Volume Dosed
absolute_total_volume_dosed:
name: Absolute Total Volume Dosed
pump_voltage:
name: Pump Voltage
last_volume_requested:
name: Last Volume Requested
max_flow_rate:
name: Max Flow Rate
script:
- id: automation_test
then:
@ -487,3 +511,33 @@ display:
lambda: |-
it.print("81818181");
text_sensor:
- platform: ezo_pmp
dosing_mode:
name: Dosing Mode
calibration_status:
name: Calibration Status
on_value:
- ezo_pmp.dose_volume:
id: hcl_pump
volume: 10
- ezo_pmp.dose_volume_over_time:
id: hcl_pump
volume: 10
duration: 2
- ezo_pmp.dose_with_constant_flow_rate:
id: hcl_pump
volume_per_minute: 10
duration: 2
- ezo_pmp.set_calibration_volume:
id: hcl_pump
volume: 10
- ezo_pmp.find: hcl_pump
- ezo_pmp.dose_continuously: hcl_pump
- ezo_pmp.clear_total_volume_dosed: hcl_pump
- ezo_pmp.clear_calibration: hcl_pump
- ezo_pmp.pause_dosing: hcl_pump
- ezo_pmp.stop_dosing: hcl_pump
- ezo_pmp.arbitrary_command:
id: hcl_pump
command: D,?

39
tests/test6.yaml Normal file
View File

@ -0,0 +1,39 @@
---
esphome:
name: test6
project:
name: esphome.test6_project
version: "1.0.0"
rp2040:
board: rpipicow
framework:
# Waiting for https://github.com/platformio/platform-raspberrypi/pull/36
platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git
wifi:
networks:
- ssid: "MySSID"
password: "password1"
api:
ota:
logger:
binary_sensor:
- platform: gpio
pin: GPIO5
id: pin_5_button
output:
- platform: gpio
pin: GPIO4
id: pin_4
switch:
- platform: output
output: pin_4
id: pin_4_switch