/* * (c) 2004,2006 Richard Titmuss for SlimProtoLib * (c) Philippe G. 2019, philippe_44@outlook.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include #include #include #include #include "esp_log.h" #include "config.h" #include "tools.h" #include "display.h" // here we should include all possible drivers extern struct display_s SSD13x6_display; struct display_s *display = NULL; static const char *TAG = "display"; #define min(a,b) (((a) < (b)) ? (a) : (b)) #define max(a,b) (((a) > (b)) ? (a) : (b)) #define DISPLAYER_STACK_SIZE (3*1024) #define SCROLLABLE_SIZE 384 #define HEADER_SIZE 64 #define DEFAULT_SLEEP 3600 static EXT_RAM_ATTR struct { TaskHandle_t task; SemaphoreHandle_t mutex; int pause, speed, by; enum { DISPLAYER_DOWN, DISPLAYER_IDLE, DISPLAYER_ACTIVE } state; char header[HEADER_SIZE + 1]; char string[SCROLLABLE_SIZE + 1]; int offset, boundary; char *metadata_config; bool timer, refresh; uint32_t elapsed, duration; TickType_t tick; } displayer; static void displayer_task(void *args); /**************************************************************************************** * */ void display_init(char *welcome) { bool init = false; char *item = config_alloc_get(NVS_TYPE_STR, "display_config"); if (item && *item) { char * drivername=strstr(item,"driver"); if (!drivername || (drivername && (strcasestr(drivername,"SSD1306") || strcasestr(drivername,"SSD1326") || strcasestr(drivername,"SH1106")))) { display = &SSD13x6_display; if (display->init(item, welcome)) { init = true; ESP_LOGI(TAG, "Display initialization successful"); } else { display = NULL; ESP_LOGW(TAG, "Display initialization failed"); } } else { ESP_LOGE(TAG,"Unknown display driver name in display config: %s",item); } } else { ESP_LOGI(TAG, "no display"); } if (init) { static DRAM_ATTR StaticTask_t xTaskBuffer __attribute__ ((aligned (4))); static EXT_RAM_ATTR StackType_t xStack[DISPLAYER_STACK_SIZE] __attribute__ ((aligned (4))); // start the task that will handle scrolling & counting displayer.mutex = xSemaphoreCreateMutex(); displayer.by = 2; displayer.pause = 3600; displayer.speed = 33; displayer.task = xTaskCreateStatic( (TaskFunction_t) displayer_task, "displayer_thread", DISPLAYER_STACK_SIZE, NULL, ESP_TASK_PRIO_MIN + 1, xStack, &xTaskBuffer); // set lines for "fixed" text mode display->set_font(1, DISPLAY_FONT_LINE_1, -3); display->set_font(2, DISPLAY_FONT_LINE_2, -3); displayer.metadata_config = config_alloc_get(NVS_TYPE_STR, "metadata_config"); } if (item) free(item); } /**************************************************************************************** * This is not thread-safe as displayer_task might be in the middle of line drawing * but it won't crash (I think) and making it thread-safe would be complicated for a * feature which is secondary (the LMS version of scrolling is thread-safe) */ static void displayer_task(void *args) { int scroll_sleep = 0, timer_sleep; while (1) { // suspend ourselves if nothing to do if (displayer.state < DISPLAYER_ACTIVE) { if (displayer.state == DISPLAYER_IDLE) display->line(2, 0, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.string); vTaskSuspend(NULL); scroll_sleep = 0; display->clear(true); display->line(1, DISPLAY_LEFT, DISPLAY_UPDATE, displayer.header); } else if (displayer.refresh) { // little trick when switching master while in IDLE and missing it display->line(1, DISPLAY_LEFT, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.header); displayer.refresh = false; } // we have been waken up before our requested time if (scroll_sleep <= 10) { // something to scroll (or we'll wake-up every pause ms ... no big deal) if (*displayer.string && displayer.state == DISPLAYER_ACTIVE) { xSemaphoreTake(displayer.mutex, portMAX_DELAY); // need to work with local copies as we don't want to suspend caller int offset = -displayer.offset; char *string = strdup(displayer.string); scroll_sleep = displayer.offset ? displayer.speed : displayer.pause; displayer.offset = displayer.offset >= displayer.boundary ? 0 : (displayer.offset + min(displayer.by, displayer.boundary - displayer.offset)); xSemaphoreGive(displayer.mutex); // now display using safe copies, can be lengthy display->line(2, offset, DISPLAY_CLEAR | DISPLAY_UPDATE, string); free(string); } else { scroll_sleep = DEFAULT_SLEEP; } } // handler elapsed track time if (displayer.timer && displayer.state == DISPLAYER_ACTIVE) { char counter[12]; TickType_t tick = xTaskGetTickCount(); uint32_t elapsed = (tick - displayer.tick) * portTICK_PERIOD_MS; if (elapsed >= 1000) { xSemaphoreTake(displayer.mutex, portMAX_DELAY); displayer.tick = tick; displayer.elapsed += elapsed / 1000; xSemaphoreGive(displayer.mutex); if (displayer.elapsed < 3600) sprintf(counter, "%5u:%02u", displayer.elapsed / 60, displayer.elapsed % 60); else sprintf(counter, "%2u:%02u:%02u", displayer.elapsed / 3600, (displayer.elapsed % 3600) / 60, displayer.elapsed % 60); display->line(1, DISPLAY_RIGHT, (DISPLAY_CLEAR | DISPLAY_ONLY_EOL) | DISPLAY_UPDATE, counter); timer_sleep = 1000; } else timer_sleep = max(1000 - elapsed, 0); } else timer_sleep = DEFAULT_SLEEP; // then sleep the min amount of time int sleep = min(scroll_sleep, timer_sleep); ESP_LOGD(TAG, "timers s:%d t:%d", scroll_sleep, timer_sleep); scroll_sleep -= sleep; vTaskDelay(sleep / portTICK_PERIOD_MS); } } /**************************************************************************************** * */ void displayer_metadata(char *artist, char *album, char *title) { char *string = displayer.string, *p; int len = SCROLLABLE_SIZE; // need a display! if (!display) return; // just do title if there is no config set if (!displayer.metadata_config) { strncpy(displayer.string, title ? title : "", SCROLLABLE_SIZE); return; } xSemaphoreTake(displayer.mutex, portMAX_DELAY); // format metadata parameters and write them directly if ((p = strcasestr(displayer.metadata_config, "format")) != NULL) { char token[16], *q; int space = len; bool skip = false; displayer.string[0] = '\0'; p = strchr(displayer.metadata_config, '='); while (p++) { // find token and copy what's after when reaching last one if (sscanf(p, "%*[^%%]%%%[^%]%%", token) < 0) { q = strchr(p, ','); strncat(string, p, q ? min(q - p, space) : space); break; } // copy what's before token (be safe) if ((q = strchr(p, '%')) == NULL) break; // skip whatever is after a token if this token is empty if (!skip) { strncat(string, p, min(q - p, space)); space = len - strlen(string); } // then copy token's content if (!strncasecmp(q + 1, "artist", 6) && artist) strncat(string, p = artist, space); else if (!strncasecmp(q + 1, "album", 5) && album) strncat(string, p = album, space); else if (!strncasecmp(q + 1, "title", 5) && title) strncat(string, p = title, space); space = len - strlen(string); // flag to skip the data following an empty field if (*p) skip = false; else skip = true; // advance to next separator p = strchr(q + 1, '%'); } } else { strncpy(string, title ? title : "", SCROLLABLE_SIZE); } // get optional scroll speed if ((p = strcasestr(displayer.metadata_config, "speed")) != NULL) sscanf(p, "%*[^=]=%d", &displayer.speed); displayer.offset = 0; utf8_decode(displayer.string); ESP_LOGI(TAG, "playing %s", displayer.string); displayer.boundary = display->stretch(2, displayer.string, SCROLLABLE_SIZE); xSemaphoreGive(displayer.mutex); } /**************************************************************************************** * */ void displayer_scroll(char *string, int speed) { // need a display! if (!display) return; xSemaphoreTake(displayer.mutex, portMAX_DELAY); if (speed) displayer.speed = speed; displayer.offset = 0; strncpy(displayer.string, string, SCROLLABLE_SIZE); displayer.string[SCROLLABLE_SIZE] = '\0'; displayer.boundary = display->stretch(2, displayer.string, SCROLLABLE_SIZE); xSemaphoreGive(displayer.mutex); } /**************************************************************************************** * */ void displayer_timer(enum displayer_time_e mode, int elapsed, int duration) { // need a display! if (!display) return; xSemaphoreTake(displayer.mutex, portMAX_DELAY); if (elapsed >= 0) displayer.elapsed = elapsed / 1000; if (duration >= 0) displayer.duration = duration / 1000; if (displayer.timer) displayer.tick = xTaskGetTickCount(); xSemaphoreGive(displayer.mutex); } /**************************************************************************************** * See above comment */ void displayer_control(enum displayer_cmd_e cmd, ...) { va_list args; if (!display) return; va_start(args, cmd); xSemaphoreTake(displayer.mutex, portMAX_DELAY); switch(cmd) { case DISPLAYER_ACTIVATE: { char *header = va_arg(args, char*); strncpy(displayer.header, header, HEADER_SIZE); displayer.header[HEADER_SIZE] = '\0'; displayer.state = DISPLAYER_ACTIVE; displayer.timer = false; displayer.refresh = true; displayer.string[0] = '\0'; displayer.elapsed = displayer.duration = 0; displayer.offset = displayer.boundary = 0; display_bus(&displayer, DISPLAY_BUS_TAKE); vTaskResume(displayer.task); break; } case DISPLAYER_SUSPEND: // task will display the line 2 from beginning and suspend displayer.state = DISPLAYER_IDLE; display_bus(&displayer, DISPLAY_BUS_GIVE); break; case DISPLAYER_SHUTDOWN: // let the task self-suspend (we might be doing i2c_write) displayer.state = DISPLAYER_DOWN; display_bus(&displayer, DISPLAY_BUS_GIVE); break; case DISPLAYER_TIMER_RUN: if (!displayer.timer) { displayer.timer = true; displayer.tick = xTaskGetTickCount(); } break; case DISPLAYER_TIMER_PAUSE: displayer.timer = false; break; default: break; } xSemaphoreGive(displayer.mutex); va_end(args); }