Rebuilt upload_tft and replaced fonts
This commit is contained in:
@@ -7,8 +7,14 @@
|
||||
|
||||
substitutions:
|
||||
|
||||
################## Defaults ##################
|
||||
# Just in case user forgets to set something #
|
||||
nextion_update_url: "https://github.com/Blackymas/NSPanel_HA_Blueprint/raw/main/nspanel_eu.tft"
|
||||
# nextion_update_blank_url: "https://github.com/Blackymas/NSPanel_HA_Blueprint/raw/main/custom_configuration/nspanel_blank.tft"
|
||||
##############################################
|
||||
|
||||
##### DON'T CHANGE THIS #####
|
||||
version: "4.0.2"
|
||||
version: "4.1dev3"
|
||||
#############################
|
||||
|
||||
##### WIFI SETUP #####
|
||||
@@ -67,10 +73,10 @@ output:
|
||||
|
||||
##### UART FOR NEXTION DISPLAY #####
|
||||
uart:
|
||||
id: tf_uart
|
||||
tx_pin: 16
|
||||
rx_pin: 17
|
||||
baud_rate: 115200
|
||||
- id: tf_uart
|
||||
tx_pin: 16
|
||||
rx_pin: 17
|
||||
baud_rate: 115200
|
||||
|
||||
##### Keeps time display updated #####
|
||||
time:
|
||||
@@ -108,7 +114,7 @@ button:
|
||||
id: nextion_init
|
||||
state: false
|
||||
- delay: 16ms
|
||||
- lambda: id(disp1).upload_tft();
|
||||
- lambda: id(upload_tft).execute("${nextion_update_url}");
|
||||
|
||||
##### EXIT REPARSE TFT DISPLAY #####
|
||||
- name: ${device_name} Exit reparse
|
||||
@@ -139,7 +145,7 @@ api:
|
||||
- binary_sensor.template.publish:
|
||||
id: nextion_init
|
||||
state: false
|
||||
- lambda: 'id(disp1)->upload_tft();'
|
||||
- lambda: 'id(upload_tft).execute("${nextion_update_url}");'
|
||||
|
||||
##### SERVICE TO UPDATE THE TFT FILE from URL #####
|
||||
- service: upload_tft_url
|
||||
@@ -150,8 +156,7 @@ api:
|
||||
- binary_sensor.template.publish:
|
||||
id: nextion_init
|
||||
state: false
|
||||
- lambda: 'id(disp1)->set_tft_url(url.c_str());'
|
||||
- lambda: 'id(disp1)->upload_tft();'
|
||||
- lambda: 'id(upload_tft).execute(url.c_str());'
|
||||
|
||||
##### Service to send a command "printf" directly to the display #####
|
||||
- service: send_command_printf
|
||||
@@ -1245,15 +1250,15 @@ text_sensor:
|
||||
id(disp1).set_component_text_printf("keyb_num.bclear", "%s", "\uE641"); //mdi:eraser-variant
|
||||
id(disp1).set_component_text_printf("keyb_num.benter", "%s", "\uE12B"); //mdi:check
|
||||
}
|
||||
else if (page=="weather01") id(disp1).set_component_text_printf("page_index", "%s", "\uE764\uE765\uE765\uE765\uE765"); // 1/5
|
||||
else if (page=="weather02") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE764\uE765\uE765\uE765"); // 2/5
|
||||
else if (page=="weather03") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE764\uE765\uE765"); // 3/5
|
||||
else if (page=="weather04") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE764\uE765"); // 4/5
|
||||
else if (page=="weather05") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE765\uE764"); // 5/5
|
||||
else if (page=="buttonpage01" or page=="entitypage01") id(disp1).set_component_text_printf("page_index", "%s", "\uE764\uE765\uE765\uE765"); // 1/4
|
||||
else if (page=="buttonpage02" or page=="entitypage02") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE764\uE765\uE765"); // 2/4
|
||||
else if (page=="buttonpage03" or page=="entitypage03") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE764\uE765"); // 3/4
|
||||
else if (page=="buttonpage04" or page=="entitypage04") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE764"); // 4/4
|
||||
else if (page=="weather01") id(disp1).set_component_text_printf("page_index", "%s", "●○○○○"); // 1/5
|
||||
else if (page=="weather02") id(disp1).set_component_text_printf("page_index", "%s", "○●○○○"); // 2/5
|
||||
else if (page=="weather03") id(disp1).set_component_text_printf("page_index", "%s", "○○●○○"); // 3/5
|
||||
else if (page=="weather04") id(disp1).set_component_text_printf("page_index", "%s", "○○○●○"); // 4/5
|
||||
else if (page=="weather05") id(disp1).set_component_text_printf("page_index", "%s", "○○○○●"); // 5/5
|
||||
else if (page=="buttonpage01" or page=="entitypage01") id(disp1).set_component_text_printf("page_index", "%s", "●○○○"); // 1/4
|
||||
else if (page=="buttonpage02" or page=="entitypage02") id(disp1).set_component_text_printf("page_index", "%s", "○●○○"); // 2/4
|
||||
else if (page=="buttonpage03" or page=="entitypage03") id(disp1).set_component_text_printf("page_index", "%s", "○○●○"); // 3/4
|
||||
else if (page=="buttonpage04" or page=="entitypage04") id(disp1).set_component_text_printf("page_index", "%s", "○○○●"); // 4/4
|
||||
else if (page=="settings")
|
||||
{
|
||||
//id(disp1).set_component_text_printf("bt_sleep", "%s", (id(sleep_mode).state) ? "\uEA19" : "\uEA18"); //mdi:toggle-switch-outline or mdi:toggle-switch-off-outline
|
||||
@@ -1665,11 +1670,13 @@ display:
|
||||
uart_id: tf_uart
|
||||
tft_url: ${nextion_update_url}
|
||||
on_page: # I couldn't make this trigger to work, so used text_sensor nspanelevent and localevent instead
|
||||
- lambda: ESP_LOGW("display.disp1", "NEXTION PAGE CHANGED");
|
||||
then:
|
||||
- lambda: ESP_LOGW("display.disp1", "NEXTION PAGE CHANGED");
|
||||
on_setup:
|
||||
then:
|
||||
- lambda: |-
|
||||
id(disp1).goto_page("boot");
|
||||
id(disp1).send_command_printf("bkcmd=3");
|
||||
id(disp1).set_component_text_printf("boot.esph_version", "%s", "${version}"); // ### esphome-version ###
|
||||
id(disp1).show_component("bt_reboot");
|
||||
id(timer_reset_all).execute("boot");
|
||||
@@ -2274,7 +2281,402 @@ script:
|
||||
else if (id(wakeup_page_name).state == "alarm") wakeup_page_id = 23;
|
||||
id(disp1).set_component_value("orign", wakeup_page_id);
|
||||
|
||||
##### ADD-ONS ############################################################
|
||||
- id: upload_tft
|
||||
mode: single
|
||||
parameters:
|
||||
url: string
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("script.upload_tft", "Starting...");
|
||||
|
||||
std::vector<uint8_t> buffer_;
|
||||
|
||||
bool is_updating_ = false;
|
||||
|
||||
uint8_t *transfer_buffer_{nullptr};
|
||||
size_t transfer_buffer_size_;
|
||||
bool upload_first_chunk_sent_ = false;
|
||||
|
||||
int content_length_ = 0;
|
||||
int tft_size_ = 0;
|
||||
|
||||
bool power_cycle_display = []()
|
||||
{
|
||||
id(screen_power).turn_off();
|
||||
delay(1500);
|
||||
id(screen_power).turn_on();
|
||||
delay(1500);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto send_nextion_command = [](const std::string &command) -> bool
|
||||
{
|
||||
ESP_LOGD("script.upload_tft.send_nextion_command", "Sending: %s", command.c_str());
|
||||
id(tf_uart).write_str(command.c_str());
|
||||
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
|
||||
id(tf_uart).write_array(to_send, sizeof(to_send));
|
||||
return true;
|
||||
};
|
||||
|
||||
auto recv_ret_string_ = [](std::string &response, uint32_t timeout, bool recv_flag) -> uint16_t
|
||||
{
|
||||
uint16_t ret;
|
||||
uint8_t c = 0;
|
||||
uint8_t nr_of_ff_bytes = 0;
|
||||
uint64_t start;
|
||||
bool exit_flag = false;
|
||||
bool ff_flag = false;
|
||||
|
||||
start = millis();
|
||||
|
||||
while ((timeout == 0 && id(tf_uart).available()) || millis() - start <= timeout)
|
||||
{
|
||||
if (!id(tf_uart).available())
|
||||
{
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
}
|
||||
|
||||
id(tf_uart).read_byte(&c);
|
||||
if (c == 0xFF)
|
||||
{
|
||||
nr_of_ff_bytes++;
|
||||
}
|
||||
else
|
||||
{
|
||||
nr_of_ff_bytes = 0;
|
||||
ff_flag = false;
|
||||
}
|
||||
|
||||
if (nr_of_ff_bytes >= 3)
|
||||
ff_flag = true;
|
||||
|
||||
response += (char) c;
|
||||
if (recv_flag)
|
||||
{
|
||||
if (response.find(0x05) != std::string::npos)
|
||||
{
|
||||
exit_flag = true;
|
||||
}
|
||||
}
|
||||
App.feed_wdt();
|
||||
delay(2);
|
||||
|
||||
if (exit_flag || ff_flag)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ff_flag)
|
||||
response = response.substr(0, response.length() - 3); // Remove last 3 0xFF
|
||||
|
||||
ret = response.length();
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto upload_end_ = [&](bool retry) -> bool
|
||||
{
|
||||
ESP_LOGD("script.upload_tft.upload_end_", "Restarting Nextion");
|
||||
send_nextion_command("rest");
|
||||
if (is_updating_) is_updating_ = not retry;
|
||||
if (retry)
|
||||
{
|
||||
ESP_LOGD("script.upload_tft.upload_end_", "Nextion TFT upload will try again");
|
||||
send_nextion_command("rest");
|
||||
delay(1500);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGD("script.upload_tft.upload_end_", "Turn off Nextion");
|
||||
//id(screen_power).turn_off();
|
||||
delay(1500);
|
||||
ESP_LOGD("script.upload_tft.upload_end_", "Restarting esphome");
|
||||
ESP.restart();
|
||||
}
|
||||
return not retry;
|
||||
};
|
||||
|
||||
auto upload_by_chunks_ = [&](HTTPClient *http, const std::string &url, int range_start) -> int
|
||||
{
|
||||
int range_end;
|
||||
|
||||
if (range_start == 0 && transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip
|
||||
range_end = 16384 - 1;
|
||||
} else {
|
||||
range_end = range_start + transfer_buffer_size_ - 1;
|
||||
}
|
||||
|
||||
if (range_end > tft_size_)
|
||||
range_end = tft_size_;
|
||||
|
||||
char range_header[64];
|
||||
sprintf(range_header, "bytes=%d-%d", range_start, range_end);
|
||||
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "Requesting range: %s", range_header);
|
||||
|
||||
int tries = 1;
|
||||
int code;
|
||||
bool begin_status;
|
||||
while (tries <= 5) {
|
||||
begin_status = http->begin(url.c_str());
|
||||
|
||||
++tries;
|
||||
if (!begin_status) {
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "upload_by_chunks_: connection failed");
|
||||
continue;
|
||||
};
|
||||
|
||||
http->addHeader("Range", range_header);
|
||||
|
||||
code = http->GET();
|
||||
if (code == 200 || code == 206) {
|
||||
break;
|
||||
}
|
||||
ESP_LOGW("script.upload_tft.upload_by_chunks", "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", url.c_str(),
|
||||
HTTPClient::errorToString(code).c_str(), tries);
|
||||
http->end();
|
||||
delay(500);
|
||||
}
|
||||
|
||||
if (tries > 5) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string recv_string;
|
||||
size_t size;
|
||||
int fetched = 0;
|
||||
int range = range_end - range_start;
|
||||
int write_len;
|
||||
|
||||
// fetch next segment from HTTP stream
|
||||
while (fetched < range) {
|
||||
size = http->getStreamPtr()->available();
|
||||
if (!size) {
|
||||
App.feed_wdt();
|
||||
delay(2);
|
||||
continue;
|
||||
}
|
||||
int c = http->getStreamPtr()->readBytes(
|
||||
&transfer_buffer_[fetched], ((size > transfer_buffer_size_) ? transfer_buffer_size_ : size));
|
||||
fetched += c;
|
||||
}
|
||||
http->end();
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "fetched %d bytes", fetched);
|
||||
|
||||
// upload fetched segments to the display in 4KB chunks
|
||||
for (int i = 0; i < range; i += 4096) {
|
||||
App.feed_wdt();
|
||||
write_len = content_length_ < 4096 ? content_length_ : 4096;
|
||||
id(tf_uart).write_array(&transfer_buffer_[i], write_len);
|
||||
content_length_ -= write_len;
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "Uploaded %0.1f %%, remaining %d Bytes",
|
||||
100.0 * (tft_size_ - content_length_) / tft_size_, content_length_);
|
||||
|
||||
if (!upload_first_chunk_sent_) {
|
||||
upload_first_chunk_sent_ = true;
|
||||
delay(500);
|
||||
}
|
||||
|
||||
recv_ret_string_(recv_string, 5000, true);
|
||||
if (recv_string[0] != 0x05) { // 0x05 == "ok"
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "recv_string [%s]",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
|
||||
}
|
||||
|
||||
// handle partial upload request
|
||||
if (recv_string[0] == 0x08 && recv_string.size() == 5) {
|
||||
uint32_t result = 0;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
|
||||
}
|
||||
if (result > 0) {
|
||||
ESP_LOGD("script.upload_tft.upload_by_chunks", "Nextion reported new range %d", result);
|
||||
content_length_ = tft_size_ - result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
recv_string.clear();
|
||||
}
|
||||
return range_end + 1;
|
||||
};
|
||||
|
||||
auto upload_tft = [&](const std::string &url, unsigned int update_baud_rate_) -> bool
|
||||
{
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Nextion TFT upload requested");
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "url: %s", url.c_str());
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "baud_rate: %i", update_baud_rate_);
|
||||
|
||||
if (is_updating_) {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Currently updating");
|
||||
return upload_end_(false);
|
||||
}
|
||||
|
||||
if (!network::is_connected()) {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "network is not connected");
|
||||
return upload_end_(false);
|
||||
}
|
||||
|
||||
send_nextion_command("DRAKJHSUYDGBNCJHGJKSHBDN");
|
||||
send_nextion_command("recmod=0");
|
||||
send_nextion_command("recmod=0");
|
||||
send_nextion_command("connect");
|
||||
|
||||
is_updating_ = true;
|
||||
|
||||
HTTPClient http;
|
||||
http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along
|
||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
bool begin_status = http.begin(url.c_str());
|
||||
|
||||
if (!begin_status) {
|
||||
is_updating_ = false;
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "connection failed");
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
allocator.deallocate(transfer_buffer_, transfer_buffer_size_);
|
||||
return upload_end_(true);
|
||||
} else {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Connected");
|
||||
}
|
||||
|
||||
http.addHeader("Range", "bytes=0-255");
|
||||
const char *header_names[] = {"Content-Range"};
|
||||
http.collectHeaders(header_names, 1);
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Requesting URL: %s", url.c_str());
|
||||
|
||||
http.setReuse(true);
|
||||
// try up to 5 times. DNS sometimes needs a second try or so
|
||||
int tries = 1;
|
||||
int code = http.GET();
|
||||
delay(100);
|
||||
|
||||
while (code != 200 && code != 206 && tries <= 5) {
|
||||
ESP_LOGW("script.upload_tft.upload_tft", "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", url.c_str(),
|
||||
HTTPClient::errorToString(code).c_str(), tries);
|
||||
|
||||
delay(250);
|
||||
code = http.GET();
|
||||
++tries;
|
||||
}
|
||||
|
||||
if ((code != 200 && code != 206) || tries > 5) {
|
||||
return upload_end_(true);
|
||||
}
|
||||
|
||||
String content_range_string = http.header("Content-Range");
|
||||
content_range_string.remove(0, 12);
|
||||
content_length_ = content_range_string.toInt();
|
||||
tft_size_ = content_length_;
|
||||
http.end();
|
||||
|
||||
if (content_length_ < 4096) {
|
||||
ESP_LOGE("script.upload_tft.upload_tft", "Failed to get file size");
|
||||
return upload_end_(true);
|
||||
}
|
||||
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Updating Nextion");
|
||||
// The Nextion will ignore the update command if it is sleeping
|
||||
|
||||
char command[128];
|
||||
// Tells the Nextion the content length of the tft file and baud rate it will be sent at
|
||||
// Once the Nextion accepts the command it will wait until the file is successfully uploaded
|
||||
// If it fails for any reason a power cycle of the display will be needed
|
||||
sprintf(command, "whmi-wris %d,%d,1", content_length_, update_baud_rate_);
|
||||
|
||||
// Clear serial receive buffer
|
||||
uint8_t d;
|
||||
while (id(tf_uart).available()) {
|
||||
id(tf_uart).read_byte(&d);
|
||||
};
|
||||
|
||||
send_nextion_command(command);
|
||||
|
||||
if (update_baud_rate_ != id(tf_uart).get_baud_rate())
|
||||
{
|
||||
id(tf_uart).set_baud_rate(update_baud_rate_);
|
||||
id(tf_uart).setup();
|
||||
}
|
||||
|
||||
std::string response;
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Waiting for upgrade response");
|
||||
recv_ret_string_(response, 15000, true); // This can take some time to return
|
||||
|
||||
// The Nextion display will, if it's ready to accept data, send a 0x05 byte.
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Upgrade response is [%s]",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str());
|
||||
|
||||
if (response.find(0x05) != std::string::npos) {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "preparation for tft update done");
|
||||
} else {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "preparation for tft update failed %d \"%s\"", response[0], response.c_str());
|
||||
return upload_end_(true);
|
||||
}
|
||||
|
||||
// Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096
|
||||
uint32_t chunk_size = 8192;
|
||||
if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand
|
||||
chunk_size = ESP.getFreeHeap() - 32768;
|
||||
chunk_size = chunk_size > 65536 ? 65536 : chunk_size;
|
||||
} else if (ESP.getFreeHeap() < 10240) {
|
||||
chunk_size = 4096;
|
||||
}
|
||||
|
||||
if (transfer_buffer_ == nullptr) {
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap());
|
||||
transfer_buffer_ = allocator.allocate(chunk_size);
|
||||
if (transfer_buffer_ == nullptr) { // Try a smaller size
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Could not allocate buffer size: %d trying 4096 instead", chunk_size);
|
||||
chunk_size = 4096;
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Allocating %d buffer", chunk_size);
|
||||
transfer_buffer_ = allocator.allocate(chunk_size);
|
||||
|
||||
if (!transfer_buffer_)
|
||||
{
|
||||
return upload_end_(true);
|
||||
}
|
||||
}
|
||||
|
||||
transfer_buffer_size_ = chunk_size;
|
||||
}
|
||||
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d",
|
||||
url.c_str(), content_length_, transfer_buffer_size_, ESP.getFreeHeap());
|
||||
|
||||
int result = 0;
|
||||
while (content_length_ > 0) {
|
||||
result = upload_by_chunks_(&http, url, result);
|
||||
if (result < 0) {
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Error updating Nextion!");
|
||||
return upload_end_(true);
|
||||
}
|
||||
App.feed_wdt();
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), content_length_);
|
||||
}
|
||||
is_updating_ = false;
|
||||
ESP_LOGD("script.upload_tft.upload_tft", "Successfully updated Nextion!");
|
||||
|
||||
return upload_end_(false);
|
||||
};
|
||||
|
||||
if (upload_tft(url, id(tf_uart).get_baud_rate())) ESP.restart();
|
||||
power_cycle_display;
|
||||
if (upload_tft(url, 921600)) ESP.restart();
|
||||
power_cycle_display;
|
||||
if (upload_tft(url, 9600)) ESP.restart();
|
||||
power_cycle_display;
|
||||
ESP_LOGE("script.upload_tft", "TFT upload failed.");
|
||||
ESP_LOGW("script.upload_tft", "Trying Nextion standard upload");
|
||||
id(disp1)->set_tft_url(url.c_str());
|
||||
id(disp1)->upload_tft();
|
||||
ESP_LOGD("script.upload_tft.upload_end_", "Restarting esphome");
|
||||
ESP.restart();
|
||||
|
||||
ESP_LOGD("script.upload_tft", "Finished!");
|
||||
|
||||
|
||||
##### ADD-ONS ############################################################
|
||||
##### Add-on - Climate #####
|
||||
- id: addon_climate_service_call
|
||||
mode: restart
|
||||
|
||||
Reference in New Issue
Block a user