Add friendly_name to device (#4296)

This commit is contained in:
Jesse Hills 2023-01-17 10:28:09 +13:00 committed by GitHub
parent 3d2d681a7b
commit c301ae3645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 137 additions and 18 deletions

View File

@ -206,6 +206,8 @@ message DeviceInfoResponse {
uint32 bluetooth_proxy_version = 11; uint32 bluetooth_proxy_version = 11;
string manufacturer = 12; string manufacturer = 12;
string friendly_name = 13;
} }
message ListEntitiesRequest { message ListEntitiesRequest {

View File

@ -930,6 +930,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
DeviceInfoResponse resp{}; DeviceInfoResponse resp{};
resp.uses_password = this->parent_->uses_password(); resp.uses_password = this->parent_->uses_password();
resp.name = App.get_name(); resp.name = App.get_name();
resp.friendly_name = App.get_friendly_name();
resp.mac_address = get_mac_address_pretty(); resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION; resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time(); resp.compilation_time = App.get_compilation_time();

View File

@ -628,6 +628,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
this->manufacturer = value.as_string(); this->manufacturer = value.as_string();
return true; return true;
} }
case 13: {
this->friendly_name = value.as_string();
return true;
}
default: default:
return false; return false;
} }
@ -645,6 +649,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(10, this->webserver_port); buffer.encode_uint32(10, this->webserver_port);
buffer.encode_uint32(11, this->bluetooth_proxy_version); buffer.encode_uint32(11, this->bluetooth_proxy_version);
buffer.encode_string(12, this->manufacturer); buffer.encode_string(12, this->manufacturer);
buffer.encode_string(13, this->friendly_name);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const { void DeviceInfoResponse::dump_to(std::string &out) const {
@ -699,6 +704,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append(" manufacturer: "); out.append(" manufacturer: ");
out.append("'").append(this->manufacturer).append("'"); out.append("'").append(this->manufacturer).append("'");
out.append("\n"); out.append("\n");
out.append(" friendly_name: ");
out.append("'").append(this->friendly_name).append("'");
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View File

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

View File

@ -1,14 +1,15 @@
from pathlib import Path from pathlib import Path
from typing import Optional
import requests import requests
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import git
from esphome.components.packages import validate_source_shorthand from esphome.components.packages import validate_source_shorthand
from esphome.const import CONF_WIFI, CONF_REF from esphome.const import CONF_REF, CONF_WIFI
from esphome.wizard import wizard_file from esphome.wizard import wizard_file
from esphome.yaml_util import dump from esphome.yaml_util import dump
from esphome import git
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
@ -66,7 +67,12 @@ async def to_code(config):
def import_config( def import_config(
path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI path: str,
name: str,
friendly_name: Optional[str],
project_name: str,
import_url: str,
network: str = CONF_WIFI,
) -> None: ) -> None:
p = Path(path) p = Path(path)
@ -77,6 +83,7 @@ def import_config(
p.write_text( p.write_text(
wizard_file( wizard_file(
name=name, name=name,
friendly_name=friendly_name,
platform="ESP32" if "esp32" in import_url else "ESP8266", platform="ESP32" if "esp32" in import_url else "ESP8266",
board="esp32dev" if "esp32" in import_url else "esp01_1m", board="esp32dev" if "esp32" in import_url else "esp01_1m",
ssid="!secret wifi_ssid", ssid="!secret wifi_ssid",
@ -98,13 +105,15 @@ def import_config(
p.write_text(req.text, encoding="utf8") p.write_text(req.text, encoding="utf8")
else: else:
substitutions = {"name": name}
esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
if friendly_name:
substitutions["friendly_name"] = friendly_name
esphome_core["friendly_name"] = "${friendly_name}"
config = { config = {
"substitutions": {"name": name}, "substitutions": substitutions,
"packages": {project_name: import_url}, "packages": {project_name: import_url},
"esphome": { "esphome": esphome_core,
"name": "${name}",
"name_add_mac_suffix": False,
},
} }
output = dump(config) output = dump(config)

View File

@ -30,6 +30,9 @@ void MDNSComponent::compile_records_() {
service.service_type = "_esphomelib"; service.service_type = "_esphomelib";
service.proto = "_tcp"; service.proto = "_tcp";
service.port = api::global_api_server->get_port(); service.port = api::global_api_server->get_port();
if (App.get_friendly_name().empty()) {
service.txt_records.push_back({"friendly_name", App.get_friendly_name()});
}
service.txt_records.push_back({"version", ESPHOME_VERSION}); service.txt_records.push_back({"version", ESPHOME_VERSION});
service.txt_records.push_back({"mac", get_mac_address()}); service.txt_records.push_back({"mac", get_mac_address()});
const char *platform = nullptr; const char *platform = nullptr;

View File

@ -104,7 +104,7 @@ void WebServer::setup() {
// Configure reconnect timeout and send config // Configure reconnect timeout and send config
client->send(json::build_json([this](JsonObject root) { client->send(json::build_json([this](JsonObject root) {
root["title"] = App.get_name(); root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root["ota"] = this->allow_ota_; root["ota"] = this->allow_ota_;
root["lang"] = "en"; root["lang"] = "en";
}).c_str(), }).c_str(),

View File

@ -260,6 +260,7 @@ CONF_FRAGMENTATION = "fragmentation"
CONF_FRAMEWORK = "framework" CONF_FRAMEWORK = "framework"
CONF_FREE = "free" CONF_FREE = "free"
CONF_FREQUENCY = "frequency" CONF_FREQUENCY = "frequency"
CONF_FRIENDLY_NAME = "friendly_name"
CONF_FROM = "from" CONF_FROM = "from"
CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_SPECTRUM = "full_spectrum"
CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_FULL_UPDATE_EVERY = "full_update_every"

View File

@ -453,6 +453,8 @@ class EsphomeCore:
self.ace = False self.ace = False
# The name of the node # The name of the node
self.name: Optional[str] = None self.name: Optional[str] = None
# The friendly name of the node
self.friendly_name: Optional[str] = None
# Additional data components can store temporary data in # Additional data components can store temporary data in
# The first key to this dict should always be the integration name # The first key to this dict should always be the integration name
self.data = {} self.data = {}
@ -492,6 +494,7 @@ class EsphomeCore:
def reset(self): def reset(self):
self.dashboard = False self.dashboard = False
self.name = None self.name = None
self.friendly_name = None
self.data = {} self.data = {}
self.config_path = None self.config_path = None
self.build_path = None self.build_path = None

View File

@ -53,13 +53,20 @@ namespace esphome {
class Application { class Application {
public: public:
void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { void pre_setup(const std::string &name, const std::string &friendly_name, const char *compilation_time,
bool name_add_mac_suffix) {
arch_init(); arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix; this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) { if (name_add_mac_suffix) {
this->name_ = name + "-" + get_mac_address().substr(6); this->name_ = name + "-" + get_mac_address().substr(6);
if (friendly_name.empty()) {
this->friendly_name_ = "";
} else {
this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6);
}
} else { } else {
this->name_ = name; this->name_ = name;
this->friendly_name_ = friendly_name;
} }
this->compilation_time_ = compilation_time; this->compilation_time_ = compilation_time;
} }
@ -134,6 +141,9 @@ class Application {
/// Get the name of this Application set by set_name(). /// Get the name of this Application set by set_name().
const std::string &get_name() const { return this->name_; } const std::string &get_name() const { return this->name_; }
/// Get the friendly name of this Application set by set_friendly_name().
const std::string &get_friendly_name() const { return this->friendly_name_; }
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
const std::string &get_compilation_time() const { return this->compilation_time_; } const std::string &get_compilation_time() const { return this->compilation_time_; }
@ -338,6 +348,7 @@ class Application {
#endif #endif
std::string name_; std::string name_;
std::string friendly_name_;
std::string compilation_time_; std::string compilation_time_;
bool name_add_mac_suffix_; bool name_add_mac_suffix_;
uint32_t last_loop_{0}; uint32_t last_loop_{0};

View File

@ -19,6 +19,7 @@ from esphome.const import (
CONF_LIBRARIES, CONF_LIBRARIES,
CONF_MIN_VERSION, CONF_MIN_VERSION,
CONF_NAME, CONF_NAME,
CONF_FRIENDLY_NAME,
CONF_ON_BOOT, CONF_ON_BOOT,
CONF_ON_LOOP, CONF_ON_LOOP,
CONF_ON_SHUTDOWN, CONF_ON_SHUTDOWN,
@ -124,6 +125,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_NAME): cv.valid_name, cv.Required(CONF_NAME): cv.valid_name,
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
cv.Optional(CONF_COMMENT): cv.string, cv.Optional(CONF_COMMENT): cv.string,
cv.Required(CONF_BUILD_PATH): cv.string, cv.Required(CONF_BUILD_PATH): cv.string,
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
@ -192,6 +194,7 @@ def preload_core_config(config, result):
conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
CORE.name = conf[CONF_NAME] CORE.name = conf[CONF_NAME]
CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME, CORE.name)
CORE.data[KEY_CORE] = {} CORE.data[KEY_CORE] = {}
if CONF_BUILD_PATH not in conf: if CONF_BUILD_PATH not in conf:
@ -346,6 +349,7 @@ async def to_code(config):
cg.add( cg.add(
cg.App.pre_setup( cg.App.pre_setup(
config[CONF_NAME], config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
cg.RawExpression('__DATE__ ", " __TIME__'), cg.RawExpression('__DATE__ ", " __TIME__'),
config[CONF_NAME_ADD_MAC_SUFFIX], config[CONF_NAME_ADD_MAC_SUFFIX],
) )

View File

@ -40,7 +40,7 @@ from esphome.storage_json import (
from esphome.util import get_serial_ports, shlex_quote from esphome.util import get_serial_ports, shlex_quote
from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf
from .util import password_hash from .util import password_hash, friendly_name_slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -390,12 +390,24 @@ class WizardRequestHandler(BaseHandler):
for k, v in json.loads(self.request.body.decode()).items() for k, v in json.loads(self.request.body.decode()).items()
if k in ("name", "platform", "board", "ssid", "psk", "password") if k in ("name", "platform", "board", "ssid", "psk", "password")
} }
if not kwargs["name"]:
self.set_status(422)
self.set_header("content-type", "application/json")
self.write(json.dumps({"error": "Name is required"}))
return
kwargs["friendly_name"] = kwargs["name"]
kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"])
kwargs["ota_password"] = secrets.token_hex(16) kwargs["ota_password"] = secrets.token_hex(16)
noise_psk = secrets.token_bytes(32) noise_psk = secrets.token_bytes(32)
kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode()
destination = settings.rel_path(f"{kwargs['name']}.yaml") filename = f"{kwargs['name']}.yaml"
destination = settings.rel_path(filename)
wizard.wizard_write(path=destination, **kwargs) wizard.wizard_write(path=destination, **kwargs)
self.set_status(200) self.set_status(200)
self.set_header("content-type", "application/json")
self.write(json.dumps({"configuration": filename}))
self.finish() self.finish()
@ -407,6 +419,7 @@ class ImportRequestHandler(BaseHandler):
args = json.loads(self.request.body.decode()) args = json.loads(self.request.body.decode())
try: try:
name = args["name"] name = args["name"]
friendly_name = args.get("friendly_name")
imported_device = next( imported_device = next(
(res for res in IMPORT_RESULT.values() if res.device_name == name), None (res for res in IMPORT_RESULT.values() if res.device_name == name), None
@ -414,12 +427,15 @@ class ImportRequestHandler(BaseHandler):
if imported_device is not None: if imported_device is not None:
network = imported_device.network network = imported_device.network
if friendly_name is None:
friendly_name = imported_device.friendly_name
else: else:
network = const.CONF_WIFI network = const.CONF_WIFI
import_config( import_config(
settings.rel_path(f"{name}.yaml"), settings.rel_path(f"{name}.yaml"),
name, name,
friendly_name,
args["project_name"], args["project_name"],
args["package_import_url"], args["package_import_url"],
network, network,
@ -434,6 +450,8 @@ class ImportRequestHandler(BaseHandler):
return return
self.set_status(200) self.set_status(200)
self.set_header("content-type", "application/json")
self.write(json.dumps({"configuration": f"{name}.yaml"}))
self.finish() self.finish()
@ -581,6 +599,12 @@ class DashboardEntry:
return self.filename.replace(".yml", "").replace(".yaml", "") return self.filename.replace(".yml", "").replace(".yaml", "")
return self.storage.name return self.storage.name
@property
def friendly_name(self):
if self.storage is None:
return self.name
return self.storage.friendly_name
@property @property
def comment(self): def comment(self):
if self.storage is None: if self.storage is None:
@ -628,6 +652,7 @@ class ListDevicesHandler(BaseHandler):
"configured": [ "configured": [
{ {
"name": entry.name, "name": entry.name,
"friendly_name": entry.friendly_name,
"configuration": entry.filename, "configuration": entry.filename,
"loaded_integrations": entry.loaded_integrations, "loaded_integrations": entry.loaded_integrations,
"deployed_version": entry.update_old, "deployed_version": entry.update_old,
@ -643,6 +668,7 @@ class ListDevicesHandler(BaseHandler):
"importable": [ "importable": [
{ {
"name": res.device_name, "name": res.device_name,
"friendly_name": res.friendly_name,
"package_import_url": res.package_import_url, "package_import_url": res.package_import_url,
"project_name": res.project_name, "project_name": res.project_name,
"project_version": res.project_version, "project_version": res.project_version,

View File

@ -1,4 +1,7 @@
import hashlib import hashlib
import unicodedata
from esphome.const import ALLOWED_NAME_CHARS
def password_hash(password: str) -> bytes: def password_hash(password: str) -> bytes:
@ -7,3 +10,23 @@ def password_hash(password: str) -> bytes:
Note this is not meant for secure storage, but for securely comparing passwords. Note this is not meant for secure storage, but for securely comparing passwords.
""" """
return hashlib.sha256(password.encode()).digest() return hashlib.sha256(password.encode()).digest()
def strip_accents(value):
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
def friendly_name_slugify(value):
value = (
strip_accents(value)
.lower()
.replace(" ", "-")
.replace("_", "-")
.replace("--", "-")
.strip("-")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)

View File

@ -36,6 +36,7 @@ class StorageJSON:
self, self,
storage_version, storage_version,
name, name,
friendly_name,
comment, comment,
esphome_version, esphome_version,
src_version, src_version,
@ -51,6 +52,8 @@ class StorageJSON:
self.storage_version: int = storage_version self.storage_version: int = storage_version
# The name of the node # The name of the node
self.name: str = name self.name: str = name
# The friendly name of the node
self.friendly_name: str = friendly_name
# The comment of the node # The comment of the node
self.comment: str = comment self.comment: str = comment
# The esphome version this was compiled with # The esphome version this was compiled with
@ -77,6 +80,7 @@ class StorageJSON:
return { return {
"storage_version": self.storage_version, "storage_version": self.storage_version,
"name": self.name, "name": self.name,
"friendly_name": self.friendly_name,
"comment": self.comment, "comment": self.comment,
"esphome_version": self.esphome_version, "esphome_version": self.esphome_version,
"src_version": self.src_version, "src_version": self.src_version,
@ -106,6 +110,7 @@ class StorageJSON:
return StorageJSON( return StorageJSON(
storage_version=1, storage_version=1,
name=esph.name, name=esph.name,
friendly_name=esph.friendly_name,
comment=esph.comment, comment=esph.comment,
esphome_version=const.__version__, esphome_version=const.__version__,
src_version=1, src_version=1,
@ -118,10 +123,13 @@ class StorageJSON:
) )
@staticmethod @staticmethod
def from_wizard(name: str, address: str, platform: str) -> "StorageJSON": def from_wizard(
name: str, friendly_name: str, address: str, platform: str
) -> "StorageJSON":
return StorageJSON( return StorageJSON(
storage_version=1, storage_version=1,
name=name, name=name,
friendly_name=friendly_name,
comment=None, comment=None,
esphome_version=None, esphome_version=None,
src_version=1, src_version=1,
@ -139,6 +147,7 @@ class StorageJSON:
storage = json.load(f_handle) storage = json.load(f_handle)
storage_version = storage["storage_version"] storage_version = storage["storage_version"]
name = storage.get("name") name = storage.get("name")
friendly_name = storage.get("friendly_name")
comment = storage.get("comment") comment = storage.get("comment")
esphome_version = storage.get( esphome_version = storage.get(
"esphome_version", storage.get("esphomeyaml_version") "esphome_version", storage.get("esphomeyaml_version")
@ -153,6 +162,7 @@ class StorageJSON:
return StorageJSON( return StorageJSON(
storage_version, storage_version,
name, name,
friendly_name,
comment, comment,
esphome_version, esphome_version,
src_version, src_version,

View File

@ -46,6 +46,11 @@ BASE_CONFIG = """esphome:
name: {name} name: {name}
""" """
BASE_CONFIG_FRIENDLY = """esphome:
name: {name}
friendly_name: {friendly_name}
"""
LOGGER_API_CONFIG = """ LOGGER_API_CONFIG = """
# Enable logging # Enable logging
logger: logger:
@ -110,7 +115,12 @@ def wizard_file(**kwargs):
kwargs["fallback_name"] = ap_name kwargs["fallback_name"] = ap_name
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
config = BASE_CONFIG.format(**kwargs) if kwargs.get("friendly_name"):
base = BASE_CONFIG_FRIENDLY
else:
base = BASE_CONFIG
config = base.format(**kwargs)
config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs)
@ -192,7 +202,7 @@ def wizard_write(path, **kwargs):
hardware = kwargs["platform"] hardware = kwargs["platform"]
write_file(path, wizard_file(**kwargs)) write_file(path, wizard_file(**kwargs))
storage = StorageJSON.from_wizard(name, f"{name}.local", hardware) storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
storage.save(storage_path) storage.save(storage_path)

View File

@ -119,10 +119,12 @@ TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url"
TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_NAME = b"project_name"
TXT_RECORD_PROJECT_VERSION = b"project_version" TXT_RECORD_PROJECT_VERSION = b"project_version"
TXT_RECORD_NETWORK = b"network" TXT_RECORD_NETWORK = b"network"
TXT_RECORD_FRIENDLY_NAME = b"friendly_name"
@dataclass @dataclass
class DiscoveredImport: class DiscoveredImport:
friendly_name: Optional[str]
device_name: str device_name: str
package_import_url: str package_import_url: str
project_name: str project_name: str
@ -174,8 +176,12 @@ class DashboardImportDiscovery:
project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode()
project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode()
network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode() network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode()
friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME)
if friendly_name is not None:
friendly_name = friendly_name.decode()
self.import_state[name] = DiscoveredImport( self.import_state[name] = DiscoveredImport(
friendly_name=friendly_name,
device_name=node_name, device_name=node_name,
package_import_url=import_url, package_import_url=import_url,
project_name=project_name, project_name=project_name,

View File

@ -9,7 +9,7 @@ pyserial==3.5
platformio==6.1.5 # When updating platformio, also update Dockerfile platformio==6.1.5 # When updating platformio, also update Dockerfile
esptool==4.4 esptool==4.4
click==8.1.3 click==8.1.3
esphome-dashboard==20221231.0 esphome-dashboard==20230117.0
aioesphomeapi==13.0.2 aioesphomeapi==13.0.2
zeroconf==0.47.1 zeroconf==0.47.1

View File

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