diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index c1e7643..d573a1c 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -109,6 +109,7 @@ esp_err_t platform_display_show_png(const uint8_t *png, uint32_t timeout_ms, char *err, size_t err_len); +void platform_display_mark_shared_spi_dirty(void); void platform_display_deinit(void); // ---------- voice_audio ---------- diff --git a/components/platform/internal/display_lcd_module.h b/components/platform/internal/display_lcd_module.h index aa7b5c6..e1a265f 100644 --- a/components/platform/internal/display_lcd_module.h +++ b/components/platform/internal/display_lcd_module.h @@ -23,6 +23,7 @@ esp_err_t lcd_module_show_png(const uint8_t *png, uint32_t timeout_ms, char *err, size_t err_len); +void lcd_module_mark_shared_spi_dirty(void); void lcd_module_deinit(void); #ifdef __cplusplus diff --git a/components/platform/internal/display_st7789p3.h b/components/platform/internal/display_st7789p3.h index 4e2dc02..def681b 100644 --- a/components/platform/internal/display_st7789p3.h +++ b/components/platform/internal/display_st7789p3.h @@ -79,6 +79,7 @@ esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on); esp_err_t st7789p3_set_madctl(st7789p3_handle_t panel, uint8_t madctl_val); esp_err_t st7789p3_set_invert(st7789p3_handle_t panel, bool invert_color); esp_err_t st7789p3_set_swap_color_bytes(st7789p3_handle_t panel, bool swap_color_bytes); +esp_err_t st7789p3_recover_spi_bus(st7789p3_handle_t panel); esp_err_t st7789p3_set_window( st7789p3_handle_t panel, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end); diff --git a/components/platform/src/direct_thermal_printer.c b/components/platform/src/direct_thermal_printer.c index 01f332a..c79f170 100644 --- a/components/platform/src/direct_thermal_printer.c +++ b/components/platform/src/direct_thermal_printer.c @@ -163,6 +163,8 @@ static esp_err_t config_input_pin(int gpio_num, bool pull_up, const char *name) } static esp_err_t ensure_shared_shift_pins_gpio_locked(void) { + platform_display_mark_shared_spi_dirty(); + esp_err_t err = config_output_pin(CONFIG_TQ_SPI_SHARED_MOSI_PIN, 0, "print_mosi"); if (err != ESP_OK) { return err; diff --git a/components/platform/src/display_lcd_module.c b/components/platform/src/display_lcd_module.c index bca6150..b106f52 100644 --- a/components/platform/src/display_lcd_module.c +++ b/components/platform/src/display_lcd_module.c @@ -22,6 +22,8 @@ static const char *TAG = "lcd_module"; #define LCD_PREVIEW_TIMEOUT_MS_DEFAULT 200 #define LCD_LOCK_TIMEOUT_MS 1000 #define LCD_DEFAULT_DRAW_LINES 20 +#define LCD_PREVIEW_FADE_DURATION_MS 150 +#define LCD_PREVIEW_FADE_STEPS 3 #ifndef CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ #define CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ (10 * 1000 * 1000) @@ -110,10 +112,16 @@ typedef struct { bool lvgl_inited; bool lvgl_ready; bool initialized; + bool shared_spi_dirty; } lcd_state_t; static lcd_state_t s_lcd; +void lcd_module_mark_shared_spi_dirty(void) +{ + s_lcd.shared_spi_dirty = true; +} + static int resolve_reset_pin(void) { if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) { @@ -189,13 +197,29 @@ static void lcd_free_lvgl_buffers(void) static esp_err_t lcd_recover_panel_for_shared_spi(void) { - /* - * Printer bit-bang path reconfigures shared MOSI/CLK as GPIO. - * Re-initialize panel before each preview draw to restore SPI pin routing. - */ - if (s_lcd.initialized) { - lcd_module_deinit(); + if (!s_lcd.initialized || s_lcd.panel == NULL) { + return lcd_module_init(); } + + if (!s_lcd.shared_spi_dirty) { + return ESP_OK; + } + + /* + * Direct-printer path temporarily takes over shared MOSI/CLK as GPIO. + * Recover LCD SPI routing only when printer touched shared pins. + */ + esp_err_t recover_rc = st7789p3_recover_spi_bus(s_lcd.panel); + if (recover_rc == ESP_OK) { + s_lcd.shared_spi_dirty = false; + return ESP_OK; + } + + ESP_LOGW(TAG, + "fast SPI recover failed: rc=0x%x, fallback to full panel reinit", + (unsigned)recover_rc); + + lcd_module_deinit(); return lcd_module_init(); } @@ -412,7 +436,62 @@ static esp_err_t lcd_scale_rgb565_to_preview(const uint16_t *src_pixels, return ESP_OK; } -static esp_err_t lcd_apply_preview_locked(lv_color_t bg_color, char *err, size_t err_len) +static uint16_t lcd_blend_rgb565(uint16_t from, uint16_t to, uint8_t alpha_255) +{ + uint32_t inv = (uint32_t)(255u - alpha_255); + + uint32_t fr = (from >> 11) & 0x1Fu; + uint32_t fg = (from >> 5) & 0x3Fu; + uint32_t fb = from & 0x1Fu; + + uint32_t tr = (to >> 11) & 0x1Fu; + uint32_t tg = (to >> 5) & 0x3Fu; + uint32_t tb = to & 0x1Fu; + + uint32_t r = (fr * inv + tr * alpha_255 + 127u) / 255u; + uint32_t g = (fg * inv + tg * alpha_255 + 127u) / 255u; + uint32_t b = (fb * inv + tb * alpha_255 + 127u) / 255u; + + return (uint16_t)((r << 11) | (g << 5) | b); +} + +static esp_err_t lcd_draw_preview_pixels_locked(const uint16_t *pixels, + uint16_t preview_w, + uint16_t preview_h, + char *err, + size_t err_len) +{ + if (s_lcd.panel == NULL || pixels == NULL) { + lcd_fill_err(err, err_len, "display panel not ready"); + return ESP_ERR_INVALID_STATE; + } + if (preview_w == 0 || preview_h == 0 || preview_w > LCD_PANEL_H_RES || preview_h > LCD_PANEL_V_RES) { + lcd_fill_err(err, err_len, "invalid preview size"); + return ESP_ERR_INVALID_SIZE; + } + + uint16_t x = (uint16_t)((LCD_PANEL_H_RES - preview_w) / 2u); + uint16_t y = (uint16_t)((LCD_PANEL_V_RES - preview_h) / 2u); + esp_err_t draw_rc = st7789p3_draw_bitmap(s_lcd.panel, + x, + y, + (uint16_t)(x + preview_w), + (uint16_t)(y + preview_h), + pixels); + if (draw_rc != ESP_OK) { + lcd_fill_err(err, err_len, "display draw failed"); + return draw_rc; + } + return ESP_OK; +} + +static esp_err_t lcd_apply_preview_locked(const uint16_t *prev_pixels, + uint16_t prev_w, + uint16_t prev_h, + bool clear_before_draw, + lv_color_t bg_color, + char *err, + size_t err_len) { if (s_lcd.panel == NULL || s_lcd.preview_pixels == NULL) { lcd_fill_err(err, err_len, "display panel not ready"); @@ -426,31 +505,72 @@ static esp_err_t lcd_apply_preview_locked(lv_color_t bg_color, char *err, size_t return ESP_ERR_INVALID_SIZE; } - esp_err_t draw_rc = st7789p3_fill_color(s_lcd.panel, (uint16_t)bg_color.full); - if (draw_rc != ESP_OK) { - lcd_fill_err(err, err_len, "display clear failed"); + if (clear_before_draw) { + esp_err_t clear_rc = st7789p3_fill_color(s_lcd.panel, (uint16_t)bg_color.full); + if (clear_rc != ESP_OK) { + lcd_fill_err(err, err_len, "display clear failed"); + return clear_rc; + } + esp_err_t draw_rc = lcd_draw_preview_pixels_locked(s_lcd.preview_pixels, preview_w, preview_h, err, err_len); + if (draw_rc == ESP_OK) { + ESP_LOGI(TAG, "preview drawn directly: size=%ux%u", (unsigned)preview_w, (unsigned)preview_h); + } return draw_rc; } - uint16_t x = (uint16_t)((LCD_PANEL_H_RES - preview_w) / 2u); - uint16_t y = (uint16_t)((LCD_PANEL_V_RES - preview_h) / 2u); - draw_rc = st7789p3_draw_bitmap(s_lcd.panel, - x, - y, - (uint16_t)(x + preview_w), - (uint16_t)(y + preview_h), - s_lcd.preview_pixels); - if (draw_rc != ESP_OK) { - lcd_fill_err(err, err_len, "display draw failed"); + bool can_fade = (prev_pixels != NULL && prev_w == preview_w && prev_h == preview_h); + if (!can_fade) { + esp_err_t draw_rc = lcd_draw_preview_pixels_locked(s_lcd.preview_pixels, preview_w, preview_h, err, err_len); + if (draw_rc == ESP_OK) { + ESP_LOGI(TAG, "preview drawn directly: size=%ux%u", (unsigned)preview_w, (unsigned)preview_h); + } return draw_rc; } - ESP_LOGI(TAG, "preview drawn directly: size=%ux%u offset=(%u,%u)", - (unsigned)preview_w, - (unsigned)preview_h, - (unsigned)x, - (unsigned)y); - return ESP_OK; + size_t pixel_count = (size_t)preview_w * (size_t)preview_h; + uint16_t *fade_pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (fade_pixels == NULL) { + fade_pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (fade_pixels == NULL) { + // Fallback to direct draw when memory is tight. + esp_err_t draw_rc = lcd_draw_preview_pixels_locked(s_lcd.preview_pixels, preview_w, preview_h, err, err_len); + if (draw_rc == ESP_OK) { + ESP_LOGI(TAG, "preview drawn directly (fade fallback): size=%ux%u", (unsigned)preview_w, (unsigned)preview_h); + } + return draw_rc; + } + + const uint32_t frame_count = LCD_PREVIEW_FADE_STEPS + 1u; + uint32_t frame_delay_ms = LCD_PREVIEW_FADE_DURATION_MS / frame_count; + if (frame_delay_ms == 0) { + frame_delay_ms = 1; + } + + for (uint32_t step = 1; step <= LCD_PREVIEW_FADE_STEPS; ++step) { + uint8_t alpha = (uint8_t)((step * 255u) / frame_count); + for (size_t i = 0; i < pixel_count; ++i) { + fade_pixels[i] = lcd_blend_rgb565(prev_pixels[i], s_lcd.preview_pixels[i], alpha); + } + + esp_err_t draw_rc = lcd_draw_preview_pixels_locked(fade_pixels, preview_w, preview_h, err, err_len); + if (draw_rc != ESP_OK) { + heap_caps_free(fade_pixels); + return draw_rc; + } + vTaskDelay(pdMS_TO_TICKS(frame_delay_ms)); + } + + heap_caps_free(fade_pixels); + esp_err_t draw_rc = lcd_draw_preview_pixels_locked(s_lcd.preview_pixels, preview_w, preview_h, err, err_len); + if (draw_rc == ESP_OK) { + ESP_LOGI(TAG, "preview drawn with fade: size=%ux%u fade_ms=%u", + (unsigned)preview_w, + (unsigned)preview_h, + (unsigned)LCD_PREVIEW_FADE_DURATION_MS); + } + return draw_rc; } void lcd_module_deinit(void) @@ -547,6 +667,7 @@ esp_err_t lcd_module_init(void) } s_lcd.initialized = true; + s_lcd.shared_spi_dirty = false; ESP_LOGI(TAG, "st7789p3 ready: spi_host=%d mosi=%d clk=%d miso=%d dc=%d cs=%d rst=%d bk=%d pclk=%u mode=%u res=%ux%u madctl=0x%02x gap=%u,%u invert=%d swap_bytes=%d", ST7789_SPI_HOST, @@ -609,9 +730,12 @@ esp_err_t lcd_module_show_raster_1bpp(const uint8_t *raster, return ESP_ERR_TIMEOUT; } - if (s_lcd.preview_pixels != NULL) { - heap_caps_free(s_lcd.preview_pixels); - } + uint16_t *prev_pixels = s_lcd.preview_pixels; + uint16_t prev_w = s_lcd.preview_dsc.header.w; + uint16_t prev_h = s_lcd.preview_dsc.header.h; + bool clear_before_draw = (s_lcd.preview_pixels == NULL || + s_lcd.preview_dsc.header.w != preview_w || + s_lcd.preview_dsc.header.h != preview_h); s_lcd.preview_pixels = preview_pixels; memset(&s_lcd.preview_dsc, 0, sizeof(s_lcd.preview_dsc)); @@ -622,7 +746,16 @@ esp_err_t lcd_module_show_raster_1bpp(const uint8_t *raster, s_lcd.preview_dsc.data_size = (uint32_t)((size_t)preview_w * preview_h * sizeof(uint16_t)); s_lcd.preview_dsc.data = (const uint8_t *)s_lcd.preview_pixels; - esp_err_t draw_err = lcd_apply_preview_locked(lv_color_white(), err, err_len); + esp_err_t draw_err = lcd_apply_preview_locked(prev_pixels, + prev_w, + prev_h, + clear_before_draw, + lv_color_white(), + err, + err_len); + if (prev_pixels != NULL) { + heap_caps_free(prev_pixels); + } lcd_lock_give(); return draw_err; #endif @@ -679,9 +812,12 @@ esp_err_t lcd_module_show_png(const uint8_t *png, return ESP_ERR_TIMEOUT; } - if (s_lcd.preview_pixels != NULL) { - heap_caps_free(s_lcd.preview_pixels); - } + uint16_t *prev_pixels = s_lcd.preview_pixels; + uint16_t prev_w = s_lcd.preview_dsc.header.w; + uint16_t prev_h = s_lcd.preview_dsc.header.h; + bool clear_before_draw = (s_lcd.preview_pixels == NULL || + s_lcd.preview_dsc.header.w != preview_w || + s_lcd.preview_dsc.header.h != preview_h); s_lcd.preview_pixels = preview_pixels; memset(&s_lcd.preview_dsc, 0, sizeof(s_lcd.preview_dsc)); @@ -692,7 +828,16 @@ esp_err_t lcd_module_show_png(const uint8_t *png, s_lcd.preview_dsc.data_size = (uint32_t)((size_t)preview_w * preview_h * sizeof(uint16_t)); s_lcd.preview_dsc.data = (const uint8_t *)s_lcd.preview_pixels; - esp_err_t draw_err = lcd_apply_preview_locked(lv_color_white(), err, err_len); + esp_err_t draw_err = lcd_apply_preview_locked(prev_pixels, + prev_w, + prev_h, + clear_before_draw, + lv_color_white(), + err, + err_len); + if (prev_pixels != NULL) { + heap_caps_free(prev_pixels); + } lcd_lock_give(); return draw_err; #endif @@ -723,9 +868,12 @@ esp_err_t lcd_module_start_demo(void) return decode_rc; } - if (s_lcd.preview_pixels != NULL) { - heap_caps_free(s_lcd.preview_pixels); - } + uint16_t *prev_pixels = s_lcd.preview_pixels; + uint16_t prev_w = s_lcd.preview_dsc.header.w; + uint16_t prev_h = s_lcd.preview_dsc.header.h; + bool clear_before_draw = (s_lcd.preview_pixels == NULL || + s_lcd.preview_dsc.header.w != demo_w || + s_lcd.preview_dsc.header.h != demo_h); s_lcd.preview_pixels = demo_pixels; memset(&s_lcd.preview_dsc, 0, sizeof(s_lcd.preview_dsc)); @@ -736,7 +884,16 @@ esp_err_t lcd_module_start_demo(void) s_lcd.preview_dsc.data_size = (uint32_t)((size_t)demo_w * demo_h * sizeof(uint16_t)); s_lcd.preview_dsc.data = (const uint8_t *)s_lcd.preview_pixels; - esp_err_t draw_err = lcd_apply_preview_locked(lv_color_black(), decode_err, sizeof(decode_err)); + esp_err_t draw_err = lcd_apply_preview_locked(prev_pixels, + prev_w, + prev_h, + clear_before_draw, + lv_color_black(), + decode_err, + sizeof(decode_err)); + if (prev_pixels != NULL) { + heap_caps_free(prev_pixels); + } lcd_lock_give(); if (draw_err != ESP_OK) { ESP_LOGW(TAG, "demo draw failed: rc=0x%x msg=%s", (unsigned)draw_err, decode_err); diff --git a/components/platform/src/display_st7789.c b/components/platform/src/display_st7789.c index ba19935..88ff964 100644 --- a/components/platform/src/display_st7789.c +++ b/components/platform/src/display_st7789.c @@ -36,3 +36,8 @@ esp_err_t platform_display_show_png(const uint8_t *png, { return lcd_module_show_png(png, png_len, timeout_ms, err, err_len); } + +void platform_display_mark_shared_spi_dirty(void) +{ + lcd_module_mark_shared_spi_dirty(); +} diff --git a/components/platform/src/display_st7789p3.c b/components/platform/src/display_st7789p3.c index 9f8997a..09aec65 100644 --- a/components/platform/src/display_st7789p3.c +++ b/components/platform/src/display_st7789p3.c @@ -199,6 +199,53 @@ static esp_err_t st7789p3_run_init_cmds(st7789p3_handle_t panel) return ESP_OK; } +static esp_err_t st7789p3_attach_spi_device(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (panel->cfg.init_spi_bus) { + spi_bus_config_t buscfg = { + .mosi_io_num = panel->cfg.pin_mosi, + .miso_io_num = panel->cfg.pin_miso, + .sclk_io_num = panel->cfg.pin_sclk, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = panel->cfg.max_transfer_sz, + }; + + esp_err_t ret = spi_bus_initialize(panel->cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); + if (ret == ESP_OK) { + panel->bus_owned = true; + } else if (ret == ESP_ERR_INVALID_STATE) { + panel->bus_owned = false; + ESP_LOGW(TAG, "SPI host already initialized, skip bus init"); + } else { + return ret; + } + } else { + panel->bus_owned = false; + } + + spi_device_interface_config_t devcfg = { + .clock_speed_hz = panel->cfg.pclk_hz, + .mode = panel->cfg.spi_mode, + .spics_io_num = panel->cfg.pin_cs, + .queue_size = panel->cfg.queue_size, + }; + + esp_err_t ret = spi_bus_add_device(panel->cfg.spi_host, &devcfg, &panel->spi); + if (ret != ESP_OK) { + if (panel->bus_owned) { + (void)spi_bus_free(panel->cfg.spi_host); + panel->bus_owned = false; + } + return ret; + } + return ESP_OK; +} + esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t *ret_panel) { if (ret_panel == NULL) { @@ -233,39 +280,8 @@ esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t return ret; } - if (panel->cfg.init_spi_bus) { - spi_bus_config_t buscfg = { - .mosi_io_num = panel->cfg.pin_mosi, - .miso_io_num = panel->cfg.pin_miso, - .sclk_io_num = panel->cfg.pin_sclk, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = panel->cfg.max_transfer_sz, - }; - - ret = spi_bus_initialize(panel->cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); - if (ret == ESP_OK) { - panel->bus_owned = true; - } else if (ret == ESP_ERR_INVALID_STATE) { - ESP_LOGW(TAG, "SPI host already initialized, skip bus init"); - } else { - free(panel); - return ret; - } - } - - spi_device_interface_config_t devcfg = { - .clock_speed_hz = panel->cfg.pclk_hz, - .mode = panel->cfg.spi_mode, - .spics_io_num = panel->cfg.pin_cs, - .queue_size = panel->cfg.queue_size, - }; - - ret = spi_bus_add_device(panel->cfg.spi_host, &devcfg, &panel->spi); + ret = st7789p3_attach_spi_device(panel); if (ret != ESP_OK) { - if (panel->bus_owned) { - spi_bus_free(panel->cfg.spi_host); - } free(panel); return ret; } @@ -297,6 +313,42 @@ esp_err_t st7789p3_del(st7789p3_handle_t panel) return ret; } +esp_err_t st7789p3_recover_spi_bus(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = st7789p3_configure_gpio(panel); + if (ret != ESP_OK) { + return ret; + } + + if (panel->spi != NULL) { + ret = spi_bus_remove_device(panel->spi); + if (ret != ESP_OK) { + return ret; + } + panel->spi = NULL; + } + + if (panel->bus_owned) { + ret = spi_bus_free(panel->cfg.spi_host); + if (ret != ESP_OK) { + return ret; + } + panel->bus_owned = false; + } + + ret = st7789p3_attach_spi_device(panel); + if (ret != ESP_OK) { + return ret; + } + + // Keep current frame on panel; just ensure display state remains on. + return st7789p3_send_cmd(panel, 0x29); +} + esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on) { if (panel == NULL) { diff --git a/components/platform/src/voice_audio.c b/components/platform/src/voice_audio.c index 2c8aa55..9b6a8fa 100644 --- a/components/platform/src/voice_audio.c +++ b/components/platform/src/voice_audio.c @@ -52,6 +52,7 @@ static bool s_opened; static uint32_t s_sample_rate; static uint8_t s_channels; static uint8_t s_bits_per_sample; +static bool s_i2s_need_enable_before_open; static void fill_err(char *err, size_t err_len, const char *msg) { if (err != NULL && err_len > 0) { @@ -302,6 +303,29 @@ static esp_err_t voice_audio_prepare(const voice_audio_open_config_t *cfg) { return ESP_OK; } +static esp_err_t voice_audio_reenable_i2s_if_needed(void) { + if (!s_i2s_need_enable_before_open) { + return ESP_OK; + } + if (s_i2s_tx == NULL || s_i2s_rx == NULL) { + s_i2s_need_enable_before_open = false; + return ESP_OK; + } + + esp_err_t tx_rc = i2s_channel_enable(s_i2s_tx); + if (tx_rc != ESP_OK && tx_rc != ESP_ERR_INVALID_STATE) { + return tx_rc; + } + + esp_err_t rx_rc = i2s_channel_enable(s_i2s_rx); + if (rx_rc != ESP_OK && rx_rc != ESP_ERR_INVALID_STATE) { + return rx_rc; + } + + s_i2s_need_enable_before_open = false; + return ESP_OK; +} + esp_err_t voice_audio_open(const voice_audio_open_config_t *cfg, char *err, size_t err_len) { if (cfg == NULL || cfg->sample_rate == 0 || cfg->channels == 0 || cfg->bits_per_sample != 16) { fill_err(err, err_len, "invalid voice audio config"); @@ -345,6 +369,13 @@ esp_err_t voice_audio_open(const voice_audio_open_config_t *cfg, char *err, size return ESP_ERR_INVALID_STATE; } + esp_err_t i2s_rc = voice_audio_reenable_i2s_if_needed(); + if (i2s_rc != ESP_OK) { + fill_err(err, err_len, "i2s re-enable failed"); + xSemaphoreGive(s_lock); + return i2s_rc; + } + esp_codec_dev_sample_info_t fs = { .sample_rate = cfg->sample_rate, .channel = cfg->channels, @@ -392,6 +423,7 @@ void voice_audio_close(void) { (void)esp_codec_dev_set_out_mute(s_codec_dev, true); (void)esp_codec_dev_close(s_codec_dev); s_opened = false; + s_i2s_need_enable_before_open = true; } xSemaphoreGive(s_lock);