mirror of
https://github.com/esphome/esphome.git
synced 2025-11-17 07:15:48 +00:00
Compare commits
13 Commits
switch_to_
...
libretiny_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9d903773 | ||
|
|
d60c358f48 | ||
|
|
26b820272a | ||
|
|
9557c90c20 | ||
|
|
d64bcf27b3 | ||
|
|
d5d61546e7 | ||
|
|
554cdbd5a4 | ||
|
|
0d147e5d10 | ||
|
|
730a70ee8b | ||
|
|
d096f1192d | ||
|
|
88a23acc4b | ||
|
|
950dff1a38 | ||
|
|
61eddfdcda |
@@ -51,14 +51,13 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
@@ -96,14 +95,13 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
|
||||
@@ -365,8 +365,10 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
|
||||
@@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
uint16_t buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer);
|
||||
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
|
||||
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer, buffer_at);
|
||||
}
|
||||
|
||||
// Reset the recursion guard for this task
|
||||
@@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
|
||||
// Save the offset before calling format_log_to_buffer_with_terminator_
|
||||
// since it will increment tx_buffer_at_ to the end of the formatted string
|
||||
uint32_t msg_start = this->tx_buffer_at_;
|
||||
uint16_t msg_start = this->tx_buffer_at_;
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
|
||||
// Write to console and send callback starting at the msg_start
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
size_t msg_length =
|
||||
uint16_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
|
||||
// Callbacks get message first (before console write)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
|
||||
// Write to console starting at the msg_start
|
||||
this->write_tx_buffer_to_console_(msg_start, &msg_length);
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -209,9 +212,7 @@ void Logger::process_messages_() {
|
||||
// This ensures all log messages appear on the console in a clean, serialized manner
|
||||
// Note: Messages may appear slightly out of order due to async processing, but
|
||||
// this is preferred over corrupted/interleaved console output
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_);
|
||||
}
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
} else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
|
||||
@@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Platform-specific: does write_msg_ add its own newline?
|
||||
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
|
||||
// Allows single write call with newline included for efficiency
|
||||
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
|
||||
// Newline should NOT be added to buffer
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
|
||||
#else
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -173,7 +184,7 @@ class Logger : public Component {
|
||||
|
||||
protected:
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg);
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
@@ -200,6 +211,35 @@ class Logger : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to add newline to buffer for platforms that need it
|
||||
// Modifies buffer_at to include the newline
|
||||
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
|
||||
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
|
||||
// Add newline - don't need to maintain null termination
|
||||
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
|
||||
// This is safe because:
|
||||
// 1. Callbacks already received the message (before we add newline)
|
||||
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
|
||||
if (*buffer_at < buffer_size) {
|
||||
buffer[(*buffer_at)++] = '\n';
|
||||
} else if (buffer_size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
buffer[buffer_size - 1] = '\n';
|
||||
*buffer_at = buffer_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to write tx_buffer_ to console if logging is enabled
|
||||
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
|
||||
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
|
||||
if (this->baud_rate_ > 0) {
|
||||
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
|
||||
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
|
||||
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format and send a log message to both console and callbacks
|
||||
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args) {
|
||||
@@ -208,10 +248,11 @@ class Logger : public Component {
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
|
||||
}
|
||||
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
|
||||
|
||||
// Console gets message WITH newline (if platform needs it)
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
|
||||
// Write the body of the log message to the buffer
|
||||
@@ -425,7 +466,9 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
|
||||
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
|
||||
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
|
||||
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
|
||||
*buffer_at += formatted_len;
|
||||
|
||||
// Remove all trailing newlines right after formatting
|
||||
|
||||
@@ -121,25 +121,23 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
if (
|
||||
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
|
||||
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Length is now always passed explicitly - no strlen() fallback needed
|
||||
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
// USB CDC/JTAG - single write including newline (already in buffer)
|
||||
// Use fwrite to stdout which goes through VFS to USB console
|
||||
//
|
||||
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
|
||||
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
|
||||
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
|
||||
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
|
||||
// to configure correctly for their hardware. This approach eliminates runtime overhead.
|
||||
fwrite(msg, 1, len, stdout);
|
||||
#else
|
||||
/* DISABLES CODE */ (false) // NOLINT
|
||||
// Regular UART - single write including newline (already in buffer)
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
#endif
|
||||
) {
|
||||
puts(msg);
|
||||
} else {
|
||||
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
|
||||
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
uart_write_bytes(this->uart_num_, "\n", 1);
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
|
||||
@@ -33,7 +33,10 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
this->hw_serial_->write(msg, len);
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
@@ -49,7 +49,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -27,7 +27,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -62,7 +62,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
|
||||
@@ -46,14 +46,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
|
||||
|
||||
// execute this script using a tuple that contains the arguments
|
||||
void execute_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
// Internal function to give scripts readable names.
|
||||
void set_name(const LogString *name) { name_ = name; }
|
||||
|
||||
protected:
|
||||
template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->execute(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
|
||||
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
|
||||
this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
}
|
||||
}
|
||||
|
||||
template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->trigger(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
|
||||
while (!this->param_queue_.empty()) {
|
||||
auto ¶ms = this->param_queue_.front();
|
||||
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
|
||||
this->param_queue_.pop_front();
|
||||
}
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
@@ -321,7 +321,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,26 +11,10 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// C++20 std::index_sequence is now used for tuple unpacking
|
||||
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
|
||||
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
|
||||
// Remove before 2026.6.0
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
|
||||
template<int N, int... S>
|
||||
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
|
||||
: gens<N - 1, N - 1, S...> {};
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; };
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
template<int...> struct seq {}; // NOLINT
|
||||
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
|
||||
template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT
|
||||
|
||||
#define TEMPLATABLE_VALUE_(type, name) \
|
||||
protected: \
|
||||
@@ -168,11 +152,11 @@ template<typename... Ts> class Condition {
|
||||
|
||||
/// Call check with a tuple of values as parameter.
|
||||
bool check_tuple(const std::tuple<Ts...> &tuple) {
|
||||
return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
return this->check(std::get<S>(tuple)...);
|
||||
}
|
||||
};
|
||||
@@ -247,11 +231,11 @@ template<typename... Ts> class Action {
|
||||
}
|
||||
}
|
||||
}
|
||||
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play_next_(std::get<S>(tuple)...);
|
||||
}
|
||||
void play_next_tuple_(const std::tuple<Ts...> &tuple) {
|
||||
this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
|
||||
virtual void stop() {}
|
||||
@@ -293,9 +277,7 @@ template<typename... Ts> class ActionList {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->play_complex(x...);
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) {
|
||||
this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
}
|
||||
void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
|
||||
void stop() {
|
||||
if (this->actions_begin_ != nullptr)
|
||||
this->actions_begin_->stop_complex();
|
||||
@@ -316,7 +298,7 @@ template<typename... Ts> class ActionList {
|
||||
}
|
||||
|
||||
protected:
|
||||
template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
|
||||
template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
|
||||
this->play(std::get<S>(tuple)...);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user