display.c 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. /*
  2. * (c) Philippe G. 2019, philippe_44@outlook.com
  3. *
  4. * This software is released under the MIT License.
  5. * https://opensource.org/licenses/MIT
  6. *
  7. */
  8. #include <string.h>
  9. #include <ctype.h>
  10. #include <stdint.h>
  11. #include <arpa/inet.h>
  12. #include "esp_log.h"
  13. #include "globdefs.h"
  14. #include "config.h"
  15. #include "tools.h"
  16. #include "display.h"
  17. #include "gds.h"
  18. #include "gds_default_if.h"
  19. #include "gds_draw.h"
  20. #include "gds_text.h"
  21. #include "gds_font.h"
  22. static const char *TAG = "display";
  23. #define min(a,b) (((a) < (b)) ? (a) : (b))
  24. #define max(a,b) (((a) > (b)) ? (a) : (b))
  25. #define DISPLAYER_STACK_SIZE (3*1024)
  26. #define SCROLLABLE_SIZE 384
  27. #define HEADER_SIZE 64
  28. #define DEFAULT_SLEEP 3600
  29. static EXT_RAM_ATTR struct {
  30. TaskHandle_t task;
  31. SemaphoreHandle_t mutex;
  32. int pause, speed, by;
  33. enum { DISPLAYER_DOWN, DISPLAYER_IDLE, DISPLAYER_ACTIVE } state;
  34. char header[HEADER_SIZE + 1];
  35. char string[SCROLLABLE_SIZE + 1];
  36. int offset, boundary;
  37. char *metadata_config;
  38. bool timer, refresh;
  39. uint32_t elapsed, duration;
  40. TickType_t tick;
  41. } displayer;
  42. static void displayer_task(void *args);
  43. struct GDS_Device *display;
  44. extern GDS_DetectFunc SSD1306_Detect, SSD132x_Detect, SH1106_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect;
  45. GDS_DetectFunc *drivers[] = { SH1106_Detect, SSD1306_Detect, SSD132x_Detect, SSD1675_Detect, SSD1322_Detect, SSD1351_Detect, ST77xx_Detect, NULL };
  46. /****************************************************************************************
  47. *
  48. */
  49. void display_init(char *welcome) {
  50. bool init = false;
  51. char *config = config_alloc_get_str("display_config", CONFIG_DISPLAY_CONFIG, "N/A");
  52. int width = -1, height = -1, backlight_pin = -1;
  53. char *p, *drivername = strstr(config, "driver");
  54. if ((p = strcasestr(config, "width")) != NULL) width = atoi(strchr(p, '=') + 1);
  55. if ((p = strcasestr(config, "height")) != NULL) height = atoi(strchr(p, '=') + 1);
  56. if ((p = strcasestr(config, "back")) != NULL) backlight_pin = atoi(strchr(p, '=') + 1);
  57. // query drivers to see if we have a match
  58. ESP_LOGI(TAG, "Trying to configure display with %s", config);
  59. if (backlight_pin >= 0) {
  60. struct GDS_BacklightPWM PWMConfig = { .Channel = pwm_system.base_channel++, .Timer = pwm_system.timer, .Max = pwm_system.max, .Init = false };
  61. display = GDS_AutoDetect(drivername, drivers, &PWMConfig);
  62. } else {
  63. display = GDS_AutoDetect(drivername, drivers, NULL);
  64. }
  65. // so far so good
  66. if (display && width > 0 && height > 0) {
  67. int RST_pin = -1;
  68. if ((p = strcasestr(config, "reset")) != NULL) RST_pin = atoi(strchr(p, '=') + 1);
  69. // Detect driver interface
  70. if (strstr(config, "I2C") && i2c_system_port != -1) {
  71. int address = 0x3C;
  72. if ((p = strcasestr(config, "address")) != NULL) address = atoi(strchr(p, '=') + 1);
  73. init = true;
  74. GDS_I2CInit( i2c_system_port, -1, -1, i2c_system_speed ) ;
  75. GDS_I2CAttachDevice( display, width, height, address, RST_pin, backlight_pin );
  76. ESP_LOGI(TAG, "Display is I2C on port %u", address);
  77. } else if (strstr(config, "SPI") && spi_system_host != -1) {
  78. int CS_pin = -1, speed = 0;
  79. if ((p = strcasestr(config, "cs")) != NULL) CS_pin = atoi(strchr(p, '=') + 1);
  80. if ((p = strcasestr(config, "speed")) != NULL) speed = atoi(strchr(p, '=') + 1);
  81. init = true;
  82. GDS_SPIInit( spi_system_host, spi_system_dc_gpio );
  83. GDS_SPIAttachDevice( display, width, height, CS_pin, RST_pin, backlight_pin, speed );
  84. ESP_LOGI(TAG, "Display is SPI host %u with cs:%d", spi_system_host, CS_pin);
  85. } else {
  86. display = NULL;
  87. ESP_LOGI(TAG, "Unsupported display interface or serial link not configured");
  88. }
  89. } else {
  90. display = NULL;
  91. ESP_LOGW(TAG, "No display driver");
  92. }
  93. if (init) {
  94. static DRAM_ATTR StaticTask_t xTaskBuffer __attribute__ ((aligned (4)));
  95. static EXT_RAM_ATTR StackType_t xStack[DISPLAYER_STACK_SIZE] __attribute__ ((aligned (4)));
  96. GDS_SetLayout( display, strcasestr(config, "HFlip"), strcasestr(config, "VFlip"), strcasestr(config, "rotate"));
  97. GDS_SetFont(display, &Font_droid_sans_fallback_15x17 );
  98. GDS_TextPos(display, GDS_FONT_MEDIUM, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, welcome);
  99. // start the task that will handle scrolling & counting
  100. displayer.mutex = xSemaphoreCreateMutex();
  101. displayer.by = 2;
  102. displayer.pause = 3600;
  103. displayer.speed = 33;
  104. displayer.task = xTaskCreateStatic( (TaskFunction_t) displayer_task, "displayer_thread", DISPLAYER_STACK_SIZE, NULL, ESP_TASK_PRIO_MIN + 1, xStack, &xTaskBuffer);
  105. // set lines for "fixed" text mode
  106. GDS_TextSetFontAuto(display, 1, GDS_FONT_LINE_1, -3);
  107. GDS_TextSetFontAuto(display, 2, GDS_FONT_LINE_2, -3);
  108. displayer.metadata_config = config_alloc_get(NVS_TYPE_STR, "metadata_config");
  109. }
  110. free(config);
  111. }
  112. /****************************************************************************************
  113. * This is not thread-safe as displayer_task might be in the middle of line drawing
  114. * but it won't crash (I think) and making it thread-safe would be complicated for a
  115. * feature which is secondary (the LMS version of scrolling is thread-safe)
  116. */
  117. static void displayer_task(void *args) {
  118. int scroll_sleep = 0, timer_sleep;
  119. while (1) {
  120. // suspend ourselves if nothing to do
  121. if (displayer.state < DISPLAYER_ACTIVE) {
  122. if (displayer.state == DISPLAYER_IDLE) GDS_TextLine(display, 2, 0, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, displayer.string);
  123. vTaskSuspend(NULL);
  124. scroll_sleep = 0;
  125. GDS_ClearExt(display, true);
  126. GDS_TextLine(display, 1, GDS_TEXT_LEFT, GDS_TEXT_UPDATE, displayer.header);
  127. } else if (displayer.refresh) {
  128. // little trick when switching master while in IDLE and missing it
  129. GDS_TextLine(display, 1, GDS_TEXT_LEFT, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, displayer.header);
  130. displayer.refresh = false;
  131. }
  132. // we have been waken up before our requested time
  133. if (scroll_sleep <= 10) {
  134. // something to scroll (or we'll wake-up every pause ms ... no big deal)
  135. if (*displayer.string && displayer.state == DISPLAYER_ACTIVE) {
  136. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  137. // need to work with local copies as we don't want to suspend caller
  138. int offset = -displayer.offset;
  139. char *string = strdup(displayer.string);
  140. scroll_sleep = displayer.offset ? displayer.speed : displayer.pause;
  141. displayer.offset = displayer.offset >= displayer.boundary ? 0 : (displayer.offset + min(displayer.by, displayer.boundary - displayer.offset));
  142. xSemaphoreGive(displayer.mutex);
  143. // now display using safe copies, can be lengthy
  144. GDS_TextLine(display, 2, offset, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, string);
  145. free(string);
  146. } else {
  147. scroll_sleep = DEFAULT_SLEEP;
  148. }
  149. }
  150. // handler elapsed track time
  151. if (displayer.timer && displayer.state == DISPLAYER_ACTIVE) {
  152. char counter[16];
  153. TickType_t tick = xTaskGetTickCount();
  154. uint32_t elapsed = (tick - displayer.tick) * portTICK_PERIOD_MS;
  155. if (elapsed >= 1000) {
  156. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  157. displayer.tick = tick;
  158. displayer.elapsed += elapsed / 1000;
  159. xSemaphoreGive(displayer.mutex);
  160. if (displayer.elapsed < 3600) snprintf(counter, 16, "%5u:%02u", displayer.elapsed / 60, displayer.elapsed % 60);
  161. else snprintf(counter, 16, "%2u:%02u:%02u", displayer.elapsed / 3600, (displayer.elapsed % 3600) / 60, displayer.elapsed % 60);
  162. GDS_TextLine(display, 1, GDS_TEXT_RIGHT, (GDS_TEXT_CLEAR | GDS_TEXT_CLEAR_EOL) | GDS_TEXT_UPDATE, counter);
  163. timer_sleep = 1000;
  164. } else timer_sleep = max(1000 - elapsed, 0);
  165. } else timer_sleep = DEFAULT_SLEEP;
  166. // then sleep the min amount of time
  167. int sleep = min(scroll_sleep, timer_sleep);
  168. ESP_LOGD(TAG, "timers s:%d t:%d", scroll_sleep, timer_sleep);
  169. scroll_sleep -= sleep;
  170. vTaskDelay(sleep / portTICK_PERIOD_MS);
  171. }
  172. }
  173. /****************************************************************************************
  174. *
  175. */
  176. void displayer_metadata(char *artist, char *album, char *title) {
  177. char *string = displayer.string, *p;
  178. int len = SCROLLABLE_SIZE;
  179. // need a display!
  180. if (!display) return;
  181. // just do title if there is no config set
  182. if (!displayer.metadata_config) {
  183. strncpy(displayer.string, title ? title : "", SCROLLABLE_SIZE);
  184. return;
  185. }
  186. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  187. // format metadata parameters and write them directly
  188. if ((p = strcasestr(displayer.metadata_config, "format")) != NULL) {
  189. char token[16], *q;
  190. int space = len;
  191. bool skip = false;
  192. displayer.string[0] = '\0';
  193. p = strchr(displayer.metadata_config, '=');
  194. while (p++) {
  195. // find token and copy what's after when reaching last one
  196. if (sscanf(p, "%*[^%%]%%%[^%]%%", token) < 0) {
  197. q = strchr(p, ',');
  198. strncat(string, p, q ? min(q - p, space) : space);
  199. break;
  200. }
  201. // copy what's before token (be safe)
  202. if ((q = strchr(p, '%')) == NULL) break;
  203. // skip whatever is after a token if this token is empty
  204. if (!skip) {
  205. strncat(string, p, min(q - p, space));
  206. space = len - strlen(string);
  207. }
  208. // then copy token's content
  209. if (!strncasecmp(q + 1, "artist", 6) && artist) strncat(string, p = artist, space);
  210. else if (!strncasecmp(q + 1, "album", 5) && album) strncat(string, p = album, space);
  211. else if (!strncasecmp(q + 1, "title", 5) && title) strncat(string, p = title, space);
  212. space = len - strlen(string);
  213. // flag to skip the data following an empty field
  214. if (*p) skip = false;
  215. else skip = true;
  216. // advance to next separator
  217. p = strchr(q + 1, '%');
  218. }
  219. } else {
  220. strncpy(string, title ? title : "", SCROLLABLE_SIZE);
  221. }
  222. // get optional scroll speed & pause
  223. if ((p = strcasestr(displayer.metadata_config, "speed")) != NULL) sscanf(p, "%*[^=]=%d", &displayer.speed);
  224. if ((p = strcasestr(displayer.metadata_config, "pause")) != NULL) sscanf(p, "%*[^=]=%d", &displayer.pause);
  225. displayer.offset = 0;
  226. utf8_decode(displayer.string);
  227. ESP_LOGI(TAG, "playing %s", displayer.string);
  228. displayer.boundary = GDS_TextStretch(display, 2, displayer.string, SCROLLABLE_SIZE);
  229. xSemaphoreGive(displayer.mutex);
  230. }
  231. /****************************************************************************************
  232. *
  233. */
  234. void displayer_scroll(char *string, int speed, int pause) {
  235. // need a display!
  236. if (!display) return;
  237. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  238. if (speed) displayer.speed = speed;
  239. if (pause) displayer.pause = pause;
  240. displayer.offset = 0;
  241. strncpy(displayer.string, string, SCROLLABLE_SIZE);
  242. displayer.string[SCROLLABLE_SIZE] = '\0';
  243. displayer.boundary = GDS_TextStretch(display, 2, displayer.string, SCROLLABLE_SIZE);
  244. xSemaphoreGive(displayer.mutex);
  245. }
  246. /****************************************************************************************
  247. *
  248. */
  249. void displayer_timer(enum displayer_time_e mode, int elapsed, int duration) {
  250. // need a display!
  251. if (!display) return;
  252. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  253. if (elapsed >= 0) displayer.elapsed = elapsed / 1000;
  254. if (duration >= 0) displayer.duration = duration / 1000;
  255. if (displayer.timer) displayer.tick = xTaskGetTickCount();
  256. xSemaphoreGive(displayer.mutex);
  257. }
  258. /****************************************************************************************
  259. * See above comment
  260. */
  261. void displayer_control(enum displayer_cmd_e cmd, ...) {
  262. va_list args;
  263. if (!display) return;
  264. va_start(args, cmd);
  265. xSemaphoreTake(displayer.mutex, portMAX_DELAY);
  266. switch(cmd) {
  267. case DISPLAYER_ACTIVATE: {
  268. char *header = va_arg(args, char*);
  269. strncpy(displayer.header, header, HEADER_SIZE);
  270. displayer.header[HEADER_SIZE] = '\0';
  271. displayer.state = DISPLAYER_ACTIVE;
  272. displayer.timer = false;
  273. displayer.refresh = true;
  274. displayer.string[0] = '\0';
  275. displayer.elapsed = displayer.duration = 0;
  276. displayer.offset = displayer.boundary = 0;
  277. display_bus(&displayer, DISPLAY_BUS_TAKE);
  278. vTaskResume(displayer.task);
  279. break;
  280. }
  281. case DISPLAYER_SUSPEND:
  282. // task will display the line 2 from beginning and suspend
  283. displayer.state = DISPLAYER_IDLE;
  284. display_bus(&displayer, DISPLAY_BUS_GIVE);
  285. break;
  286. case DISPLAYER_SHUTDOWN:
  287. // let the task self-suspend (we might be doing i2c_write)
  288. displayer.state = DISPLAYER_DOWN;
  289. display_bus(&displayer, DISPLAY_BUS_GIVE);
  290. break;
  291. case DISPLAYER_TIMER_RUN:
  292. if (!displayer.timer) {
  293. display_bus(&displayer, DISPLAY_BUS_TAKE);
  294. displayer.timer = true;
  295. displayer.tick = xTaskGetTickCount();
  296. }
  297. break;
  298. case DISPLAYER_TIMER_PAUSE:
  299. displayer.timer = false;
  300. break;
  301. default:
  302. break;
  303. }
  304. xSemaphoreGive(displayer.mutex);
  305. va_end(args);
  306. }