display.c 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. #include "display.h"
  2. #include "rtsp_events.h"
  3. #include "u8g2.h"
  4. #include "u8g2_esp32_hal.h"
  5. #include "esp_log.h"
  6. #include "esp_timer.h"
  7. #include "freertos/FreeRTOS.h"
  8. #include "freertos/task.h"
  9. #include <stdio.h>
  10. #include <string.h>
  11. static const char *TAG = "display";
  12. // ============================================================================
  13. // Display state (protected by copy-on-event, read by render task)
  14. // ============================================================================
  15. typedef enum {
  16. DISPLAY_STATE_STANDBY,
  17. DISPLAY_STATE_CONNECTED,
  18. DISPLAY_STATE_PLAYING,
  19. DISPLAY_STATE_PAUSED,
  20. } display_state_t;
  21. static u8g2_t s_u8g2;
  22. static struct {
  23. char title[METADATA_STRING_MAX];
  24. char artist[METADATA_STRING_MAX];
  25. char album[METADATA_STRING_MAX];
  26. uint32_t duration_secs;
  27. uint32_t position_secs;
  28. display_state_t state;
  29. bool dirty; // set by event callback, cleared by render
  30. int64_t sync_time_us; // esp_timer_get_time() when position was last synced
  31. } s_display;
  32. // Scroll configuration
  33. #define SCROLL_PX_PER_TICK 2
  34. #define SCROLL_GAP_PX 30 // pixel gap before text wraps
  35. #define SCROLL_PAUSE_TICKS 3 // pause before scrolling restarts
  36. #define SCROLL_INTERVAL_MS 50 // render interval during active scroll
  37. #define PAUSE_INDICATOR_W 14 // reserved width for "||" + gap
  38. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  39. #define NUM_SCROLL_LINES 1
  40. #else
  41. #define NUM_SCROLL_LINES 3
  42. #endif
  43. static struct {
  44. int offset[NUM_SCROLL_LINES];
  45. int pause_ticks[NUM_SCROLL_LINES];
  46. bool active; // true if any line is scrolling
  47. } s_scroll;
  48. static void scroll_reset(void) {
  49. memset(&s_scroll, 0, sizeof(s_scroll));
  50. }
  51. static void scroll_restart(void) {
  52. for (int i = 0; i < NUM_SCROLL_LINES; i++) {
  53. s_scroll.offset[i] = 0;
  54. s_scroll.pause_ticks[i] = SCROLL_PAUSE_TICKS;
  55. }
  56. }
  57. // ============================================================================
  58. // Helpers
  59. // ============================================================================
  60. /**
  61. * Draw a separator bar in the gap between wrapped copies of the text.
  62. */
  63. static void draw_scroll_separator(u8g2_t *u8g2, int x, int y) {
  64. int ascent = u8g2_GetAscent(u8g2);
  65. int h = ascent;
  66. u8g2_DrawVLine(u8g2, x, y - h + 1, h);
  67. }
  68. /**
  69. * Draw a text line with continuous horizontal scrolling when content exceeds
  70. * max_w. The text wraps seamlessly: a vertical bar separator and the text's
  71. * beginning scroll in from the right before the end scrolls off the left,
  72. * creating a smooth loop with no snap-back.
  73. */
  74. static void draw_scrolling_line(u8g2_t *u8g2, int idx, int y, const char *str,
  75. int max_w) {
  76. if (!str || !str[0]) {
  77. return;
  78. }
  79. int text_w = u8g2_GetUTF8Width(u8g2, str);
  80. if (text_w <= max_w) {
  81. u8g2_DrawUTF8(u8g2, 0, y, str);
  82. s_scroll.offset[idx] = 0;
  83. return;
  84. }
  85. // Total loop length: text + gap (the gap holds the separator bar)
  86. int loop_w = text_w + SCROLL_GAP_PX;
  87. s_scroll.active = true;
  88. int ascent = u8g2_GetAscent(u8g2);
  89. int descent = u8g2_GetDescent(u8g2);
  90. u8g2_SetClipWindow(u8g2, 0, y - ascent, max_w, y - descent + 1);
  91. int off = s_scroll.offset[idx];
  92. // Draw the primary copy
  93. u8g2_DrawUTF8(u8g2, -off, y, str);
  94. // Draw separator bar in the gap
  95. draw_scroll_separator(u8g2, text_w + SCROLL_GAP_PX / 2 - off, y);
  96. // Draw the wrapped copy (appears from the right)
  97. u8g2_DrawUTF8(u8g2, loop_w - off, y, str);
  98. u8g2_SetMaxClipWindow(u8g2);
  99. if (s_scroll.pause_ticks[idx] > 0) {
  100. s_scroll.pause_ticks[idx]--;
  101. } else {
  102. s_scroll.offset[idx] += SCROLL_PX_PER_TICK;
  103. if (s_scroll.offset[idx] >= loop_w) {
  104. s_scroll.offset[idx] = 0;
  105. }
  106. }
  107. }
  108. /**
  109. * Draw the progress bar: [====> ] with time on each side.
  110. */
  111. /**
  112. * Get the estimated playback position based on wall-clock interpolation.
  113. */
  114. static uint32_t get_estimated_position(void) {
  115. uint32_t pos = s_display.position_secs;
  116. if (s_display.state == DISPLAY_STATE_PLAYING && s_display.sync_time_us > 0) {
  117. int64_t elapsed_us = esp_timer_get_time() - s_display.sync_time_us;
  118. uint32_t elapsed_secs = (uint32_t)(elapsed_us / 1000000);
  119. pos += elapsed_secs;
  120. if (s_display.duration_secs > 0 && pos > s_display.duration_secs) {
  121. pos = s_display.duration_secs;
  122. }
  123. }
  124. return pos;
  125. }
  126. static void draw_progress(u8g2_t *u8g2, int y, uint32_t pos, uint32_t dur) {
  127. char pos_str[8], dur_str[8];
  128. rtsp_format_time_mmss(pos, pos_str, sizeof(pos_str));
  129. rtsp_format_time_mmss(dur, dur_str, sizeof(dur_str));
  130. // Measure time string widths
  131. int pos_w = u8g2_GetUTF8Width(u8g2, pos_str);
  132. int dur_w = u8g2_GetUTF8Width(u8g2, dur_str);
  133. // Draw position time on the left
  134. u8g2_DrawUTF8(u8g2, 0, y, pos_str);
  135. // Draw duration time on the right
  136. u8g2_DrawUTF8(u8g2, 128 - dur_w, y, dur_str);
  137. // Progress bar between the time strings
  138. int bar_x = pos_w + 3;
  139. int bar_w = 128 - dur_w - 3 - bar_x;
  140. int bar_y = y - 5; // bar height ~5px, top-aligned to text baseline
  141. int bar_h = 5;
  142. if (bar_w > 0) {
  143. // Outline
  144. u8g2_DrawFrame(u8g2, bar_x, bar_y, bar_w, bar_h);
  145. // Fill proportional to position
  146. if (dur > 0 && pos <= dur) {
  147. int fill = (int)((uint64_t)(bar_w - 2) * pos / dur);
  148. if (fill > 0) {
  149. u8g2_DrawBox(u8g2, bar_x + 1, bar_y + 1, fill, bar_h - 2);
  150. }
  151. }
  152. }
  153. }
  154. // ============================================================================
  155. // Rendering
  156. // ============================================================================
  157. static void display_render(void) {
  158. u8g2_ClearBuffer(&s_u8g2);
  159. switch (s_display.state) {
  160. case DISPLAY_STATE_STANDBY:
  161. u8g2_SetFont(&s_u8g2, u8g2_font_7x14_tf);
  162. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  163. u8g2_DrawUTF8(&s_u8g2, 0, 20, "AirPlay Ready");
  164. #else
  165. u8g2_DrawUTF8(&s_u8g2, 0, 32, "AirPlay Ready");
  166. #endif
  167. break;
  168. case DISPLAY_STATE_CONNECTED:
  169. u8g2_SetFont(&s_u8g2, u8g2_font_7x14_tf);
  170. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  171. u8g2_DrawUTF8(&s_u8g2, 0, 20, "Connected");
  172. #else
  173. u8g2_DrawUTF8(&s_u8g2, 0, 32, "Connected");
  174. #endif
  175. break;
  176. case DISPLAY_STATE_PLAYING:
  177. case DISPLAY_STATE_PAUSED: {
  178. s_scroll.active = false;
  179. bool paused = (s_display.state == DISPLAY_STATE_PAUSED);
  180. int disp_w = u8g2_GetDisplayWidth(&s_u8g2);
  181. int top_max_w = paused ? disp_w - PAUSE_INDICATOR_W : disp_w;
  182. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  183. // Compact 2-line layout: "Title - Artist" (scrolling) + progress bar
  184. char line[METADATA_STRING_MAX * 2 + 4];
  185. const char *title = s_display.title[0] ? s_display.title : "---";
  186. const char *artist = s_display.artist[0] ? s_display.artist : "";
  187. if (artist[0]) {
  188. snprintf(line, sizeof(line), "%s - %s", title, artist);
  189. } else {
  190. snprintf(line, sizeof(line), "%s", title);
  191. }
  192. u8g2_SetFont(&s_u8g2, u8g2_font_6x13_tf);
  193. draw_scrolling_line(&s_u8g2, 0, 13, line, top_max_w);
  194. // Line 2: Progress bar
  195. u8g2_SetFont(&s_u8g2, u8g2_font_5x8_tf);
  196. draw_progress(&s_u8g2, 30, get_estimated_position(),
  197. s_display.duration_secs);
  198. #else
  199. // Full 4-line layout for 128x64 displays
  200. // Line 1: Title (larger font, clipped for pause indicator)
  201. u8g2_SetFont(&s_u8g2, u8g2_font_7x14B_tf);
  202. draw_scrolling_line(&s_u8g2, 0, 13,
  203. s_display.title[0] ? s_display.title : "---",
  204. top_max_w);
  205. // Line 2: Artist
  206. u8g2_SetFont(&s_u8g2, u8g2_font_6x13_tf);
  207. draw_scrolling_line(&s_u8g2, 1, 28,
  208. s_display.artist[0] ? s_display.artist : "", disp_w);
  209. // Line 3: Album
  210. draw_scrolling_line(&s_u8g2, 2, 42,
  211. s_display.album[0] ? s_display.album : "", disp_w);
  212. // Line 4: Progress bar with times
  213. u8g2_SetFont(&s_u8g2, u8g2_font_5x8_tf);
  214. draw_progress(&s_u8g2, 62, get_estimated_position(),
  215. s_display.duration_secs);
  216. #endif
  217. // Paused indicator (top-right)
  218. if (paused) {
  219. u8g2_SetFont(&s_u8g2, u8g2_font_5x8_tf);
  220. const char *paused_str = "||";
  221. int w = u8g2_GetUTF8Width(&s_u8g2, paused_str);
  222. u8g2_DrawUTF8(&s_u8g2, disp_w - w, 8, paused_str);
  223. }
  224. break;
  225. }
  226. }
  227. u8g2_SendBuffer(&s_u8g2);
  228. }
  229. // ============================================================================
  230. // RTSP event callback
  231. // ============================================================================
  232. static void on_rtsp_event(rtsp_event_t event, const rtsp_event_data_t *data,
  233. void *user_data) {
  234. (void)user_data;
  235. switch (event) {
  236. case RTSP_EVENT_CLIENT_CONNECTED:
  237. s_display.state = DISPLAY_STATE_CONNECTED;
  238. memset(s_display.title, 0, sizeof(s_display.title));
  239. memset(s_display.artist, 0, sizeof(s_display.artist));
  240. memset(s_display.album, 0, sizeof(s_display.album));
  241. s_display.duration_secs = 0;
  242. s_display.position_secs = 0;
  243. s_display.sync_time_us = 0;
  244. s_display.dirty = true;
  245. scroll_reset();
  246. break;
  247. case RTSP_EVENT_PLAYING:
  248. s_display.state = DISPLAY_STATE_PLAYING;
  249. s_display.sync_time_us = esp_timer_get_time();
  250. s_display.dirty = true;
  251. break;
  252. case RTSP_EVENT_PAUSED:
  253. // Freeze position at current estimate before pausing
  254. s_display.position_secs = get_estimated_position();
  255. s_display.sync_time_us = 0;
  256. s_display.state = DISPLAY_STATE_PAUSED;
  257. s_display.dirty = true;
  258. break;
  259. case RTSP_EVENT_DISCONNECTED:
  260. s_display.state = DISPLAY_STATE_STANDBY;
  261. memset(s_display.title, 0, sizeof(s_display.title));
  262. memset(s_display.artist, 0, sizeof(s_display.artist));
  263. memset(s_display.album, 0, sizeof(s_display.album));
  264. s_display.duration_secs = 0;
  265. s_display.position_secs = 0;
  266. s_display.sync_time_us = 0;
  267. s_display.dirty = true;
  268. scroll_reset();
  269. break;
  270. case RTSP_EVENT_METADATA:
  271. if (data) {
  272. // Detect a real track change so we know when position=0 is legitimate
  273. // (start of a new track) vs. a spurious mid-song reset from AirPlay.
  274. bool track_changed = data->metadata.title[0] &&
  275. strncmp(s_display.title, data->metadata.title,
  276. METADATA_STRING_MAX) != 0;
  277. // Only overwrite text fields when the event actually carries them;
  278. // progress-only updates arrive with zeroed strings.
  279. if (data->metadata.title[0]) {
  280. memcpy(s_display.title, data->metadata.title, METADATA_STRING_MAX);
  281. }
  282. if (data->metadata.artist[0]) {
  283. memcpy(s_display.artist, data->metadata.artist, METADATA_STRING_MAX);
  284. }
  285. if (data->metadata.album[0]) {
  286. memcpy(s_display.album, data->metadata.album, METADATA_STRING_MAX);
  287. }
  288. if (data->metadata.duration_secs) {
  289. s_display.duration_secs = data->metadata.duration_secs;
  290. }
  291. // AirPlay occasionally emits position_secs=0 mid-song without a track
  292. // change, which would reset the progress bar. Only accept 0 on an
  293. // actual track change; otherwise ignore it and keep the current
  294. // interpolated position.
  295. if (data->metadata.position_secs != 0 || track_changed) {
  296. s_display.position_secs = data->metadata.position_secs;
  297. s_display.sync_time_us = esp_timer_get_time();
  298. }
  299. s_display.dirty = true;
  300. scroll_restart();
  301. }
  302. break;
  303. }
  304. }
  305. // ============================================================================
  306. // Display task
  307. // ============================================================================
  308. static void display_task(void *pvParameters) {
  309. (void)pvParameters;
  310. const TickType_t interval = pdMS_TO_TICKS(CONFIG_DISPLAY_UPDATE_MS);
  311. const TickType_t one_sec = pdMS_TO_TICKS(1000);
  312. const TickType_t scroll_interval = pdMS_TO_TICKS(SCROLL_INTERVAL_MS);
  313. // Initial render
  314. display_render();
  315. while (1) {
  316. bool is_playing = (s_display.state == DISPLAY_STATE_PLAYING);
  317. if (s_scroll.active) {
  318. vTaskDelay(scroll_interval);
  319. s_display.dirty = false;
  320. display_render();
  321. continue;
  322. }
  323. if (is_playing) {
  324. vTaskDelay(one_sec);
  325. s_display.dirty = false;
  326. display_render();
  327. continue;
  328. }
  329. vTaskDelay(interval);
  330. if (s_display.dirty) {
  331. s_display.dirty = false;
  332. display_render();
  333. }
  334. }
  335. }
  336. // ============================================================================
  337. // Initialization
  338. // ============================================================================
  339. void display_init(void *bus) {
  340. #if defined(CONFIG_DISPLAY_BUS_SPI)
  341. ESP_LOGI(
  342. TAG, "Initializing OLED display (SPI: CLK=%d MOSI=%d CS=%d DC=%d RST=%d)",
  343. CONFIG_DISPLAY_SPI_CLK, CONFIG_DISPLAY_SPI_MOSI, CONFIG_DISPLAY_SPI_CS,
  344. CONFIG_DISPLAY_SPI_DC, CONFIG_DISPLAY_SPI_RST);
  345. u8g2_esp32_hal_t hal = U8G2_ESP32_HAL_DEFAULT;
  346. if (bus == NULL) {
  347. hal.bus.spi.clk = CONFIG_DISPLAY_SPI_CLK;
  348. hal.bus.spi.mosi = CONFIG_DISPLAY_SPI_MOSI;
  349. }
  350. hal.bus.spi.cs = CONFIG_DISPLAY_SPI_CS;
  351. hal.dc = CONFIG_DISPLAY_SPI_DC;
  352. hal.reset = CONFIG_DISPLAY_SPI_RST;
  353. u8g2_esp32_hal_init(hal);
  354. if (bus != NULL) {
  355. u8g2_esp32_hal_set_spi_host((spi_host_device_t)(intptr_t)bus);
  356. }
  357. // Setup u8g2 for the selected display driver and height (SPI)
  358. #if defined(CONFIG_DISPLAY_DRIVER_SH1106)
  359. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  360. u8g2_Setup_sh1106_128x32_visionox_f(&s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb,
  361. u8g2_esp32_gpio_and_delay_cb);
  362. #else
  363. u8g2_Setup_sh1106_128x64_noname_f(&s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb,
  364. u8g2_esp32_gpio_and_delay_cb);
  365. #endif
  366. #elif defined(CONFIG_DISPLAY_DRIVER_SSD1309)
  367. u8g2_Setup_ssd1309_128x64_noname0_f(&s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb,
  368. u8g2_esp32_gpio_and_delay_cb);
  369. #elif defined(CONFIG_DISPLAY_DRIVER_SH1107)
  370. u8g2_Setup_sh1107_seeed_128x128_f(&s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb,
  371. u8g2_esp32_gpio_and_delay_cb);
  372. #else // SSD1306 (default)
  373. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  374. u8g2_Setup_ssd1306_128x32_univision_f(
  375. &s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  376. #else
  377. u8g2_Setup_ssd1306_128x64_vcomh0_f(&s_u8g2, U8G2_R0, u8g2_esp32_spi_byte_cb,
  378. u8g2_esp32_gpio_and_delay_cb);
  379. #endif
  380. #endif
  381. #else // I2C (default)
  382. ESP_LOGI(TAG, "Initializing OLED display (I2C: SDA=%d SCL=%d addr=0x%02x)",
  383. CONFIG_DISPLAY_I2C_SDA, CONFIG_DISPLAY_I2C_SCL,
  384. CONFIG_DISPLAY_I2C_ADDR);
  385. // Bus must be supplied by the board; display cannot init without one.
  386. if (bus == NULL) {
  387. ESP_LOGW(TAG, "No I2C bus supplied — display disabled");
  388. return;
  389. }
  390. i2c_master_bus_handle_t i2c_bus = (i2c_master_bus_handle_t)bus;
  391. // Probe for the display before attempting init. OLED controllers can take
  392. // up to 100ms to boot after power-on, so retry a few times with delays.
  393. bool display_found = false;
  394. for (int attempt = 0; attempt < 5; attempt++) {
  395. if (attempt > 0) {
  396. vTaskDelay(pdMS_TO_TICKS(50));
  397. i2c_master_bus_reset(i2c_bus);
  398. }
  399. if (i2c_master_probe(i2c_bus, CONFIG_DISPLAY_I2C_ADDR, 100) == ESP_OK) {
  400. display_found = true;
  401. break;
  402. }
  403. ESP_LOGD(TAG, "Display probe attempt %d failed", attempt + 1);
  404. }
  405. if (!display_found) {
  406. ESP_LOGW(
  407. TAG,
  408. "No OLED display at I2C addr 0x%02x after retries — display disabled",
  409. CONFIG_DISPLAY_I2C_ADDR);
  410. i2c_master_bus_reset(i2c_bus);
  411. // Scan the bus so the caller can see what is actually present
  412. ESP_LOGW(TAG, "Scanning I2C bus for devices...");
  413. bool found_any = false;
  414. for (uint8_t addr = 0x08; addr < 0x78; addr++) {
  415. if (i2c_master_probe(i2c_bus, addr, 10) == ESP_OK) {
  416. ESP_LOGW(TAG, " found device at 0x%02x", addr);
  417. found_any = true;
  418. }
  419. }
  420. if (!found_any) {
  421. ESP_LOGW(TAG, " no devices found on I2C bus");
  422. }
  423. return;
  424. }
  425. // Configure the ESP32 HAL for I2C
  426. u8g2_esp32_hal_t hal = U8G2_ESP32_HAL_DEFAULT;
  427. u8g2_esp32_hal_init(hal);
  428. u8g2_esp32_hal_set_i2c_bus(i2c_bus);
  429. // Setup u8g2 for the selected display driver and height (I2C)
  430. #if defined(CONFIG_DISPLAY_DRIVER_SH1106)
  431. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  432. u8g2_Setup_sh1106_i2c_128x32_visionox_f(
  433. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  434. #else
  435. u8g2_Setup_sh1106_i2c_128x64_noname_f(
  436. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  437. #endif
  438. #elif defined(CONFIG_DISPLAY_DRIVER_SSD1309)
  439. u8g2_Setup_ssd1309_i2c_128x64_noname0_f(
  440. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  441. #elif defined(CONFIG_DISPLAY_DRIVER_SH1107)
  442. u8g2_Setup_sh1107_i2c_seeed_128x128_f(
  443. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  444. #else // SSD1306 (default)
  445. #if defined(CONFIG_DISPLAY_HEIGHT_32)
  446. u8g2_Setup_ssd1306_i2c_128x32_univision_f(
  447. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  448. #else
  449. u8g2_Setup_ssd1306_i2c_128x64_noname_f(
  450. &s_u8g2, U8G2_R0, u8g2_esp32_i2c_byte_cb, u8g2_esp32_gpio_and_delay_cb);
  451. #endif
  452. #endif
  453. // Set I2C address (u8x8 expects left-shifted 7-bit address)
  454. u8x8_SetI2CAddress(&s_u8g2.u8x8, CONFIG_DISPLAY_I2C_ADDR << 1);
  455. #endif // DISPLAY_BUS
  456. u8g2_InitDisplay(&s_u8g2);
  457. u8g2_SetPowerSave(&s_u8g2, 0);
  458. #ifdef CONFIG_DISPLAY_FLIP
  459. u8g2_SetFlipMode(&s_u8g2, 1);
  460. #endif
  461. u8g2_ClearBuffer(&s_u8g2);
  462. u8g2_SendBuffer(&s_u8g2);
  463. // Initialize state
  464. s_display.state = DISPLAY_STATE_STANDBY;
  465. s_display.dirty = true;
  466. // Register for RTSP events
  467. rtsp_events_register(on_rtsp_event, NULL);
  468. // Start display refresh task
  469. xTaskCreate(display_task, "display", 4096, NULL, 3, NULL);
  470. ESP_LOGI(TAG, "OLED display initialized");
  471. }