{"id":4907,"date":"2025-06-28T08:00:25","date_gmt":"2025-06-28T15:00:25","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4907"},"modified":"2025-06-20T05:48:03","modified_gmt":"2025-06-20T12:48:03","slug":"esp32-cam-hud-or-chud","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4907","title":{"rendered":"ESP32-Cam Cheap Heads Up Display or C.H.U.D."},"content":{"rendered":"<p>This post is a continuation from an earlier post that introduced the concept of writing pixel data to the ESP32-Cam module frame buffer.\u00a0 The buffer can be streamed or written as an image file to the microSD media.\u00a0 In this post I&#8217;ll expand on text and graphics with a HUD styled overlay.\u00a0 I&#8217;ll be sticking with saving images sequences and not covering streaming here.<\/p>\n<p>The code in this post was generated and refined using OpenAI&#8217;s ChatGPT.\u00a0 I won&#8217;t provide the semantics of the prompts here, but will cover some key items that were discovered during the development of the code.\u00a0 Here is a demo video of the HUD with pitch, roll, and yaw values derived from a time variable.<\/p>\n<p><iframe loading=\"lazy\" title=\"ESP32-Cam HUD Demo\" src=\"https:\/\/player.vimeo.com\/video\/1094378769?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479\" width=\"640\" height=\"480\" frameborder=\"0\"><\/iframe><\/p>\n<p>Getting to this point took several steps and extensive debugging.\u00a0 AI&#8217;s interpretation of my prompts was a game of back and forth.\u00a0 There were two essential issues that I discovered which required more work than I anticipated.\u00a0 The first was including a simple degree symbol in the text string.\u00a0 The code generated by AI was stuck on the UTF-8 encoding value being 176.\u00a0 After some brute testing with all ASCII codes, I found that 123 was the correct value.\u00a0 Here are my notes on that phase of the development.<\/p>\n<pre>AI replied:\r\n\"\u00b0\" is a UTF-8 encoded degree symbol in the source string.\r\n{0x06,0x09,0x09,0x06,0x00}, \/\/ \u00b0 (176)\r\n\r\nI prompted:\r\nThese are the mappings for ASCII number value to actual character ... \u00b0 = 123\r\n\r\nAI replied:\r\nThis confirms the real issue:\r\nThe degree symbol (\u00b0) is mapped to ASCII code 123 in your setup \u2014 not 176.<\/pre>\n<p>There was more back and forth than this, but I just want to point out that it was odd to have a response so fundamentally flawed.\u00a0 I was left to suspect it was similar to speed governor you would find on a rental truck so you can&#8217;t drive 120 mph on the highway.<\/p>\n<p>The next strange item was the color space debugging.\u00a0 In the code provided, writing to the buffer requires working in RGB565 color space.\u00a0 The frames are then saved to the jpg format.\u00a0 I had issues with this in my earlier post with the images having a psychedelic look because YUV channels were directly set to RGB channels.\u00a0 The color space needs to be correctly converted, not just interchanged.\u00a0 That was all well and good, but AI was responding with a macro that had the blue and red channels reversed.<\/p>\n<pre>AI replied:\r\nYou are using RGB565 format, a common color format for displays with 16 bits per pixel\r\n5 bits red (0..31)\r\n6 bits green (0..63)\r\n5 bits blue (0..31)\r\n\r\nHelper macro \u2014 easy way to convert 8-bit RGB \u2192 RGB565:\r\nIf you want to define colors using normal 0..255 values, use this macro:\r\n#define RGB565(r8, g8, b8) ( ((r8 &amp; 0xF8) &lt;&lt; 8) | ((g8 &amp; 0xFC) &lt;&lt; 3) | (b8 &gt;&gt; 3) )\r\n\r\nI prompted:\r\nBut the colors are actually blue, green, red\r\n\r\nuint16_t blue = RGB565(255, 0, 0); \/\/ Blue\r\nuint16_t green = RGB565(0, 255, 0); \/\/ Green\r\nuint16_t red = RGB565(0, 0, 255); \/\/ Red<\/pre>\n<p>I had to stop from the HUD project and create a new project to debug the character and color issue.\u00a0 Here is a video of the final debug that sorted out the character and color issue.<\/p>\n<p><iframe loading=\"lazy\" title=\"ESP32-Cam HUD Demo\" src=\"https:\/\/player.vimeo.com\/video\/1094379690?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479\" width=\"640\" height=\"480\" frameborder=\"0\"><\/iframe><\/p>\n<p>The frame rate was another issue.\u00a0 I suspected that IDE or some library was introducing a bottleneck.\u00a0 AI confirmed this by pointing out regressions from newer libraries that were intended to provide compatiblity.\u00a0 Here is AI&#8217;s response to my prompt.<\/p>\n<pre>I prompted: \r\nThis leads me to believe that the board library or the IDE is introducing the bottleneck... Provide me sources of the core regressions mentioned, specific to many JPEG\/SDMMC slowdowns reported.\r\nChatGPT said:\r\n\r\nAI Replied:\r\nHere are some specific sources and background regarding the ESP32 Arduino Core regressions that have impacted JPEG and SD\/MMC performance in recent versions. This is an issue I track closely, because many users run into this exact problem when doing image\/video work with ESP32-CAM and SD.\r\n\r\n<a href=\"https:\/\/github.com\/espressif\/esp32-camera\/issues\/537\">https:\/\/github.com\/espressif\/esp32-camera\/issues\/537<\/a>\r\n\"Low fps on ESP32-CAM after upgrading to newer core\".\r\n\u2192 Users reporting fps drops after upgrading from 1.0.x \u2192 2.0.x.\r\n\r\n<a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/issues\/6609\">https:\/\/github.com\/espressif\/arduino-esp32\/issues\/6609<\/a>\r\n\"SD_MMC performance regression\"\r\n\u2192 Reports of SDMMC writes becoming 2-5\u00d7 slower after Core 2.0.5+.\r\n\r\n<a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/issues\/7221\">https:\/\/github.com\/espressif\/arduino-esp32\/issues\/7221<\/a>\r\n\"SDMMC issues with camera\"\r\n\u2192 Many users confirm 1.0.6 works great, but 2.x series is slower for SDMMC + JPEG.\r\n\r\n<a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/issues\/7255\">https:\/\/github.com\/espressif\/arduino-esp32\/issues\/7255<\/a>\r\n\"JPEG encoder slow performance on newer core\"\r\n\u2192 JPEG encode latency increased in 2.x series.\r\n\r\nThe main reasons seen in these reports:\r\n\r\nCamera driver moved to I2S DMA double buffering \u2192 which adds some latency when used with SDMMC.\r\nSDMMC driver changed default clock to 10 MHz instead of 20-40 MHz for \"compatibility\" \u2192 silent regression.\r\nHeap allocation pattern changed \u2192 increased fragmentation, leading to malloc stalls.\r\nJPEG encoder changes in esp32-camera fork used by 2.x sometimes switch to software path when heap is low \u2192 big fps hit.<\/pre>\n<p>So, I made some changes, but ultimately settled with what I had and decided to save this effort for another time.\u00a0 With that all out of the way, I concluded with this code for the C.H.U.D.<\/p>\n<pre>\/*\r\n\r\nESP32-CAM_Video_ver14c\r\n\r\nThis program captures data only to the video frame\r\nThe code was developed using ChatGPT\r\n\r\nEdited by Patrick Gilfeather June 28 2025\r\n\r\nSoftware used\r\n- Arduino IDE version 1.8.19\r\n- ESP32 core version 2.0.4 \r\n- Board AI Thinker ESP32-CAM\r\n- Partition Scheme Huge APP (3MB No OTA)\r\n\r\nHardware used\r\n- Espressif ESP32-Cam module\r\n- SanDisk Ultra 64GB microSD class 10 format FAT (32-bit version)\r\n\r\n\r\n*\/\r\n\r\n\r\n#include \"FS.h\"\r\n#include \"SD_MMC.h\"\r\n#include \"esp_camera.h\"\r\n#include \"img_converters.h\"\r\n\r\n\r\n#define FRAME_WIDTH 320\r\n#define FRAME_HEIGHT 240\r\n#define FRAME_INTERVAL_MS 250 \/\/ Frame delay\r\n\r\nuint32_t frameCounter = 0;\r\nunsigned long lastFrameTime = 0;\r\n\r\nunsigned long cycleDuration = 60000; \/\/ 60 seconds in milliseconds\r\n\r\n#define RGB565(r8, g8, b8) ( ((r8 &amp; 0xF8) &lt;&lt; 8) | ((g8 &amp; 0xFC) &lt;&lt; 3) | (b8 &gt;&gt; 3) )\r\n\r\n\/\/ The format follows this (blue, green, red)\r\nuint16_t black = RGB565(0, 0, 0); \/\/ Black\r\nuint16_t white = RGB565(255, 255, 255); \/\/ White\r\nuint16_t blue = RGB565(255, 0, 0); \/\/ Blue\r\nuint16_t green = RGB565(0, 255, 0); \/\/ Green\r\nuint16_t red = RGB565(0, 0, 255); \/\/ Red\r\nuint16_t yellow = RGB565(0, 255, 255); \/\/ Yellow\r\nuint16_t cyan = RGB565(255, 255, 0); \/\/ Cyan\r\nuint16_t magenta = RGB565(255, 0, 255); \/\/ Magenta\r\nuint16_t orange = RGB565(0, 128, 255); \/\/ Orange\r\nuint16_t brown = RGB565(32, 32, 128); \/\/ Brown\r\nuint16_t gray = RGB565(128, 128, 128); \/\/ Gray\r\n\r\nfloat pitch = 0;\r\nfloat roll = 0;\r\nfloat yaw = 0;\r\nint len = 0;\r\n\r\nstatic const uint8_t font5x7[][5] = { \/\/ ASCII 32 to 123 (printable characters) \r\n{0x00,0x00,0x00,0x00,0x00}, \/\/ = 32\r\n{0x00,0x00,0x5F,0x00,0x00}, \/\/ ! = 33\r\n{0x00,0x07,0x00,0x07,0x00}, \/\/ \" = 34\r\n{0x14,0x7F,0x14,0x7F,0x14}, \/\/ # = 35 \r\n{0x24,0x2A,0x7F,0x2A,0x12}, \/\/ $ = 36\r\n{0x23,0x13,0x08,0x64,0x62}, \/\/ % = 37\r\n{0x36,0x49,0x55,0x22,0x50}, \/\/ &amp; = 38\r\n{0x00,0x05,0x03,0x00,0x00}, \/\/ ' = 39 \r\n{0x00,0x1C,0x22,0x41,0x00}, \/\/ ( = 40\r\n{0x00,0x41,0x22,0x1C,0x00}, \/\/ ) = 41\r\n{0x14,0x08,0x3E,0x08,0x14}, \/\/ * = 42\r\n{0x08,0x08,0x3E,0x08,0x08}, \/\/ + = 43 \r\n{0x00,0x50,0x30,0x00,0x00}, \/\/ , = 44\r\n{0x08,0x08,0x08,0x08,0x08}, \/\/ - = 45\r\n{0x00,0x60,0x60,0x00,0x00}, \/\/ . = 46\r\n{0x20,0x10,0x08,0x04,0x02}, \/\/ \/ = 47 \r\n{0x3E,0x51,0x49,0x45,0x3E}, \/\/ 0 = 48\r\n{0x00,0x42,0x7F,0x40,0x00}, \/\/ 1 = 49\r\n{0x42,0x61,0x51,0x49,0x46}, \/\/ 2 = 50\r\n{0x21,0x41,0x45,0x4B,0x31}, \/\/ 3 = 51 \r\n{0x18,0x14,0x12,0x7F,0x10}, \/\/ 4 = 52\r\n{0x27,0x45,0x45,0x45,0x39}, \/\/ 5 = 53\r\n{0x3C,0x4A,0x49,0x49,0x30}, \/\/ 6 = 54\r\n{0x01,0x71,0x09,0x05,0x03}, \/\/ 7 = 55 \r\n{0x36,0x49,0x49,0x49,0x36}, \/\/ 8 = 56\r\n{0x06,0x49,0x49,0x29,0x1E}, \/\/ 9 = 57\r\n{0x00,0x36,0x36,0x00,0x00}, \/\/ : = 58\r\n{0x00,0x56,0x36,0x00,0x00}, \/\/ ; = 59 \r\n{0x08,0x14,0x22,0x41,0x00}, \/\/ &lt; = 60\r\n{0x14,0x14,0x14,0x14,0x14}, \/\/ = = 61\r\n{0x00,0x41,0x22,0x14,0x08}, \/\/ &gt; = 62\r\n{0x02,0x01,0x51,0x09,0x06}, \/\/ ? = 63 \r\n{0x32,0x49,0x79,0x41,0x3E}, \/\/ @ = 64\r\n{0x7E,0x11,0x11,0x11,0x7E}, \/\/ A = 65\r\n{0x7F,0x49,0x49,0x49,0x36}, \/\/ B = 66\r\n{0x3E,0x41,0x41,0x41,0x22}, \/\/ C = 67 \r\n{0x7F,0x41,0x41,0x22,0x1C}, \/\/ D = 68\r\n{0x7F,0x49,0x49,0x49,0x41}, \/\/ E = 69\r\n{0x7F,0x09,0x09,0x09,0x01}, \/\/ F = 70\r\n{0x3E,0x41,0x49,0x49,0x7A}, \/\/ G = 71 \r\n{0x7F,0x08,0x08,0x08,0x7F}, \/\/ H = 72\r\n{0x00,0x41,0x7F,0x41,0x00}, \/\/ I = 73\r\n{0x20,0x40,0x41,0x3F,0x01}, \/\/ J = 74\r\n{0x7F,0x08,0x14,0x22,0x41}, \/\/ K = 75 \r\n{0x7F,0x40,0x40,0x40,0x40}, \/\/ L = 76\r\n{0x7F,0x02,0x0C,0x02,0x7F}, \/\/ M = 77\r\n{0x7F,0x04,0x08,0x10,0x7F}, \/\/ N = 78\r\n{0x3E,0x41,0x41,0x41,0x3E}, \/\/ O = 79 \r\n{0x7F,0x09,0x09,0x09,0x06}, \/\/ P = 80\r\n{0x3E,0x41,0x51,0x21,0x5E}, \/\/ Q = 81\r\n{0x7F,0x09,0x19,0x29,0x46}, \/\/ R = 82\r\n{0x46,0x49,0x49,0x49,0x31}, \/\/ S = 83 \r\n{0x01,0x01,0x7F,0x01,0x01}, \/\/ T = 84\r\n{0x3F,0x40,0x40,0x40,0x3F}, \/\/ U = 85\r\n{0x1F,0x20,0x40,0x20,0x1F}, \/\/ V = 86\r\n{0x3F,0x40,0x38,0x40,0x3F}, \/\/ W = 87 \r\n{0x63,0x14,0x08,0x14,0x63}, \/\/ X = 88\r\n{0x07,0x08,0x70,0x08,0x07}, \/\/ Y = 89\r\n{0x61,0x51,0x49,0x45,0x43}, \/\/ Z = 90\r\n{0x00,0x7F,0x41,0x41,0x00}, \/\/ [ = 91 \r\n{0x02,0x04,0x08,0x10,0x20}, \/\/ \\ = 92\r\n{0x00,0x41,0x41,0x7F,0x00}, \/\/ ] = 93\r\n{0x04,0x02,0x01,0x02,0x04}, \/\/ ^ = 94\r\n{0x40,0x40,0x40,0x40,0x40}, \/\/ _ = 95 \r\n{0x00,0x01,0x02,0x04,0x00}, \/\/ ` = 96\r\n{0x20,0x54,0x54,0x54,0x78}, \/\/ a = 97\r\n{0x7F,0x48,0x44,0x44,0x38}, \/\/ b = 98\r\n{0x38,0x44,0x44,0x44,0x20}, \/\/ c = 99 \r\n{0x38,0x44,0x44,0x48,0x7F}, \/\/ d = 100\r\n{0x38,0x54,0x54,0x54,0x18}, \/\/ e = 101\r\n{0x08,0x7E,0x09,0x01,0x02}, \/\/ f = 102\r\n{0x0C,0x52,0x52,0x52,0x3E}, \/\/ g = 103 \r\n{0x7F,0x08,0x04,0x04,0x78}, \/\/ h = 104\r\n{0x00,0x44,0x7D,0x40,0x00}, \/\/ i = 105\r\n{0x20,0x40,0x44,0x3D,0x00}, \/\/ j = 106\r\n{0x7F,0x10,0x28,0x44,0x00}, \/\/ k = 107 \r\n{0x00,0x41,0x7F,0x40,0x00}, \/\/ l = 108\r\n{0x7C,0x04,0x18,0x04,0x78}, \/\/ m = 109\r\n{0x7C,0x08,0x04,0x04,0x78}, \/\/ n = 110\r\n{0x38,0x44,0x44,0x44,0x38}, \/\/ o = 111 \r\n{0x7C,0x14,0x14,0x14,0x08}, \/\/ p = 112\r\n{0x08,0x14,0x14,0x18,0x7C}, \/\/ q = 113\r\n{0x7C,0x08,0x04,0x04,0x08}, \/\/ r = 114\r\n{0x48,0x54,0x54,0x54,0x20}, \/\/ s = 115 \r\n{0x04,0x3F,0x44,0x40,0x20}, \/\/ t = 116\r\n{0x3C,0x40,0x40,0x20,0x7C}, \/\/ u = 117\r\n{0x1C,0x20,0x40,0x20,0x1C}, \/\/ v = 118\r\n{0x3C,0x40,0x30,0x40,0x3C}, \/\/ w = 119 \r\n{0x44,0x28,0x10,0x28,0x44}, \/\/ x = 120\r\n{0x0C,0x50,0x50,0x50,0x3C}, \/\/ y = 121\r\n{0x44,0x64,0x54,0x4C,0x44}, \/\/ z = 122\r\n{0x06,0x09,0x09,0x06,0x00} \/\/ \u00b0 = 123\r\n};\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- SETUP -------------------\r\nvoid setup() {\r\ndelay(10);\r\n\r\nif (!SD_MMC.begin()) {\r\nreturn;\r\n}\r\n\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- LOOP -------------------\r\nvoid loop() {\r\n\r\nuint16_t* buffer = createBlackFrameRGB565();\r\nif (!buffer) return;\r\n\r\n\/*\r\nfloat pitch = 0; \/\/ Example pitch simulation\r\nfloat roll = 0; \/\/ Example roll simulation\r\n*\/\r\n\r\npitch = sin(millis() * 0.00001) * 15; \/\/ Example pitch simulation\r\nroll = sin(millis() * 0.00001) * 25; \/\/ Example roll simulation\r\nyaw = sin(millis() * 0.00001) * 180; \/\/ Example yaw simulation\r\n\r\ndrawPitchLadder(buffer, FRAME_WIDTH, FRAME_HEIGHT, pitch, roll);\r\ndrawBearingLine(buffer, FRAME_WIDTH, FRAME_HEIGHT, yaw, green);\r\n\r\ndrawUpperRightText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\ndrawUpperLeftText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\n\r\ndrawMidLeftText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\ndrawMidRightText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\n\r\ndrawLowerLeftText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\ndrawLowerRightText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\n\r\nchar filename[32];\r\nsprintf(filename, \"\/frame_sim_%05lu.jpg\", frameCounter++);\r\nsaveAsJPEG(buffer, FRAME_WIDTH, FRAME_HEIGHT, filename);\r\n\r\nfree(buffer);\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- FRAME CREATION -------------------\r\nuint16_t* createBlackFrameRGB565() {\r\nsize_t bufferSize = FRAME_WIDTH * FRAME_HEIGHT * sizeof(uint16_t);\r\nuint16_t* buffer = (uint16_t*)ps_malloc(bufferSize);\r\nif (!buffer) {\r\nreturn nullptr;\r\n}\r\nmemset(buffer, 0x00, bufferSize);\r\nreturn buffer;\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- OVERLAYS -------------------\r\n\r\nvoid drawPitchLadder(uint16_t* buffer, int w, int h, float pitchDegrees, float rollDegrees) {\r\nint cx = w \/ 2;\r\nint cy = h \/ 2;\r\n\r\n\/\/ Convert angles to radians and pixels\r\nfloat rollRad = rollDegrees * DEG_TO_RAD;\r\nfloat pitchPixelsPerDegree = h \/ 90.0; \/\/ scale for pitch effect\r\nfloat cosRoll = cos(rollRad);\r\nfloat sinRoll = sin(rollRad);\r\n\r\n\/\/ Calculate total vertical pitch offset\r\nint pitchOffset = (int)(pitchDegrees * pitchPixelsPerDegree);\r\n\r\nint stepDegrees = 10;\r\nint maxDegrees = 180; \/\/ extend range beyond the display to cover possible shifts\r\nint lineLength = w \/ 16;\r\n\r\nfor (int ladderDegree = -maxDegrees; ladderDegree &lt;= maxDegrees; ladderDegree += stepDegrees) {\r\nif (ladderDegree == 0) continue; \/\/ skip horizon line\r\n\r\n\/\/ Calculate ladder line vertical position including pitch offset\r\nint yOffset = (int)(-ladderDegree * pitchPixelsPerDegree) + pitchOffset;\r\nint y = cy + yOffset;\r\n\r\n\/\/ Skip lines outside the visible frame\r\nif (y &lt; 0 || y &gt;= h) continue;\r\n\r\nint x0 = cx - lineLength \/ 2;\r\nint y0 = y;\r\nint x1 = cx + lineLength \/ 2;\r\nint y1 = y;\r\n\r\nrotatePoint(x0, y0, cx, cy, cosRoll, sinRoll, &amp;x0, &amp;y0);\r\nrotatePoint(x1, y1, cx, cy, cosRoll, sinRoll, &amp;x1, &amp;y1);\r\n\r\n\r\nuint16_t color = (ladderDegree &gt; 0) ? cyan : orange;\r\n\r\n\r\ndrawLineRGB565(buffer, w, h, x0, y0, x1, y1, color);\r\n\r\n\/\/ Draw label\r\nchar label[8];\r\nsprintf(label, \"%+d\", ladderDegree);\r\nint labelX = cx + (lineLength \/ 2) + 5;\r\nint labelY = y;\r\nrotatePoint(labelX, labelY, cx, cy, cosRoll, sinRoll, &amp;labelX, &amp;labelY);\r\ndrawText(buffer, w, h, labelX, labelY, label, color);\r\n}\r\n\r\n\/\/ Draw horizon (center) line with pitch offset\r\nint horizonLength = w \/ 8;\r\nint horizonY = cy + pitchOffset; \/\/ horizon shifts with pitch\r\n\/\/ int horizonY = cy - pitchOffset; \/\/ horizon shifts with pitch\r\nint x0 = cx - horizonLength \/ 2;\r\nint y0 = horizonY;\r\nint x1 = cx + horizonLength \/ 2;\r\nint y1 = horizonY;\r\n\r\nrotatePoint(x0, y0, cx, cy, cosRoll, sinRoll, &amp;x0, &amp;y0);\r\nrotatePoint(x1, y1, cx, cy, cosRoll, sinRoll, &amp;x1, &amp;y1);\r\ndrawLineRGB565(buffer, w, h, x0, y0, x1, y1, orange);\r\n\r\n\r\n\/\/ Draw a black masking to cover ladder overlap on the buffer\r\nint rectWidth = w;\r\nint rectHeight = h - 200;\r\n\r\n\/\/ Start the rectangle's top\r\nint startX = 0;\r\nint startY = 0;\r\n\r\n\/\/ Draw the rectangle\r\nfor (int y = 0; y &lt; rectHeight; y++) {\r\nfor (int x = 0; x &lt; rectWidth; x++) {\r\nint px = startX + x;\r\nint py = startY + y;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h) {\r\nbuffer[py * w + px] = black;\r\n}\r\n}\r\n}\r\n\r\n\/\/ Start the rectangle's bottom\r\nstartX = 0;\r\nstartY = 188;\r\n\r\n\/\/ Draw the rectangle\r\nfor (int y = 0; y &lt; rectHeight; y++) {\r\nfor (int x = 0; x &lt; rectWidth; x++) {\r\nint px = startX + x;\r\nint py = startY + y;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h) {\r\nbuffer[py * w + px] = black;\r\n}\r\n}\r\n}\r\n\r\n\/\/ Start the rectangle's bottom\r\nstartX = 0;\r\nstartY = 200;\r\n\r\n\/\/ Draw the rectangle\r\nfor (int y = 0; y &lt; rectHeight; y++) {\r\nfor (int x = 0; x &lt; rectWidth; x++) {\r\nint px = startX + x;\r\nint py = startY + y;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h) {\r\nbuffer[py * w + px] = black;\r\n}\r\n}\r\n}\r\n\r\n\r\n\/\/ Aircraft symbol (rolls only)\r\n\r\nint wingSpan = w \/ 16;\r\nint noseLength = h \/ 20;\r\nint ax0 = cx - wingSpan;\r\nint ay0 = cy - 12; \/\/ Shift shape 12 pixels higher\r\nint ax1 = cx;\r\nint ay1 = ay0 + noseLength;\r\nint ax2 = cx + wingSpan;\r\nint ay2 = ay0;\r\n\r\nrotatePoint(ax0, ay0, cx, cy, cosRoll, sinRoll, &amp;ax0, &amp;ay0);\r\nrotatePoint(ax1, ay1, cx, cy, cosRoll, sinRoll, &amp;ax1, &amp;ay1);\r\nrotatePoint(ax2, ay2, cx, cy, cosRoll, sinRoll, &amp;ax2, &amp;ay2);\r\n\r\n\/\/ Draw aircraft V-shape\r\ndrawLineRGB565(buffer, w, h, ax0, ay0, ax1, ay1, red);\r\ndrawLineRGB565(buffer, w, h, ax1, ay1, ax2, ay2, green);\r\n\r\n\r\n\/\/ Draw circular dot at center\r\nint dotRadius = 3; \/\/ Radius of the circle (6-pixel diameter)\r\nfor (int dx = -dotRadius; dx &lt;= dotRadius; dx++) {\r\nfor (int dy = -dotRadius; dy &lt;= dotRadius; dy++) {\r\nif (dx*dx + dy*dy &lt;= dotRadius*dotRadius) { \/\/ Circle equation\r\nint px = cx + dx;\r\nint py = cy + dy;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h) {\r\nbuffer[py * w + px] = green; \/\/ Or use a contrasting color\r\n}\r\n}\r\n}\r\n}\r\n\r\n\/\/ Draw circular dot at center\r\ndotRadius = 2; \/\/ Radius of the circle (4-pixel diameter)\r\nfor (int dx = -dotRadius; dx &lt;= dotRadius; dx++) {\r\nfor (int dy = -dotRadius; dy &lt;= dotRadius; dy++) {\r\nif (dx*dx + dy*dy &lt;= dotRadius*dotRadius) { \/\/ Circle equation\r\nint px = cx + dx;\r\nint py = cy + dy;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h) {\r\nbuffer[py * w + px] = black; \/\/ Or use a contrasting color\r\n}\r\n}\r\n}\r\n}\r\n\r\n\r\n}\r\n\r\n\r\n\r\n\r\nvoid rotatePoint(int x, int y, int cx, int cy, float cosA, float sinA, int* outX, int* outY) {\r\nint dx = x - cx;\r\nint dy = y - cy;\r\n*outX = cx + (int)(dx * cosA - dy * sinA);\r\n*outY = cy + (int)(dx * sinA + dy * cosA);\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawBearingLine(uint16_t* buffer, int w, int h, float yawDegrees, uint16_t color) {\r\nint lineY = h - 32; \/\/ Fixed height, 35 pixels from bottom\r\nint centerX = w \/ 2;\r\nint lineLength = w * 0.65; \/\/ Total compass line length\r\nint halfLine = lineLength \/ 2;\r\n\r\n\/\/ Draw horizontal base line\r\nint x0 = centerX - halfLine;\r\nint x1 = centerX + halfLine;\r\ndrawLineRGB565(buffer, w, h, x0, lineY, x1, lineY, color);\r\n\r\n\/\/ Draw moving tick marks and labels\r\nint degreesPerTick = 10;\r\nint totalDegrees = 360; \/\/ Full circle\r\nint tickHeight = 5;\r\nint majorTickHeight = 10;\r\nint labelOffset = 12;\r\n\r\nfloat scaleOffsetDegrees = -yawDegrees; \/\/ Move scale with yaw\r\n\r\nfor (int deg = 0; deg &lt; totalDegrees; deg += degreesPerTick) {\r\n\/\/ Adjust degree by scale offset and wrap around\r\nfloat displayDeg = deg + scaleOffsetDegrees;\r\nwhile (displayDeg &lt; 0) displayDeg += 360;\r\nwhile (displayDeg &gt;= 360) displayDeg -= 360;\r\n\r\n\/\/ Map degrees to X position on tape\r\nfloat degFromCenter = displayDeg &gt; 180 ? displayDeg - 360 : displayDeg;\r\nfloat fraction = degFromCenter \/ 90.0f; \/\/ +\/- 90 degrees\r\nif (fraction &lt; -1.0f || fraction &gt; 1.0f) continue;\r\n\r\nint tickX = centerX + (int)(fraction * halfLine);\r\nint thisTickHeight = (deg % 30 == 0) ? majorTickHeight : tickHeight;\r\ndrawLineRGB565(buffer, w, h, tickX, lineY - thisTickHeight \/ 2, tickX, lineY + thisTickHeight \/ 2, color);\r\n\r\nif (deg % 30 == 0) {\r\n\/\/ Compass labels for cardinal points\r\nconst char* label = nullptr;\r\nswitch (deg) {\r\ncase 0: label = \"N\"; break;\r\ncase 90: label = \"E\"; break;\r\ncase 180: label = \"S\"; break;\r\ncase 270: label = \"W\"; break;\r\ndefault: {\r\nstatic char labelBuf[8];\r\nsprintf(labelBuf, \"%d\", deg);\r\nlabel = labelBuf;\r\nbreak;\r\n}\r\n}\r\n\r\nint len = strlen(label);\r\nint textX = tickX - (len * 6) \/ 2;\r\nint textY = lineY - thisTickHeight \/ 2 - labelOffset;\r\ndrawText(buffer, w, h, textX, textY, label, color);\r\n}\r\n}\r\n\r\n\/\/ Draw center triangle indicator\r\nint triHeight = 10;\r\nint triBase = 8;\r\nint topX = centerX;\r\nint topY = lineY - triHeight;\r\nint leftX = centerX - triBase \/ 2;\r\nint leftY = lineY;\r\nint rightX = centerX + triBase \/ 2;\r\nint rightY = lineY;\r\n\r\n\/\/ Draw triangle edges\r\ndrawLineRGB565(buffer, w, h, leftX, leftY, topX, topY, color);\r\ndrawLineRGB565(buffer, w, h, topX, topY, rightX, rightY, color);\r\ndrawLineRGB565(buffer, w, h, rightX, rightY, leftX, leftY, color);\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawLineRGB565(uint16_t* pixels, int w, int h, int x0, int y0, int x1, int y1, uint16_t color) {\r\nint dx = abs(x1 - x0), sx = x0 &lt; x1 ? 1 : -1;\r\nint dy = -abs(y1 - y0), sy = y0 &lt; y1 ? 1 : -1;\r\nint err = dx + dy, e2;\r\nwhile (true) {\r\nif (x0 &gt;= 0 &amp;&amp; x0 &lt; w &amp;&amp; y0 &gt;= 0 &amp;&amp; y0 &lt; h)\r\npixels[y0 * w + x0] = color;\r\nif (x0 == x1 &amp;&amp; y0 == y1) break;\r\ne2 = 2 * err;\r\nif (e2 &gt;= dy) { err += dy; x0 += sx; }\r\nif (e2 &lt;= dx) { err += dx; y0 += sy; }\r\n}\r\n}\r\n\r\n\r\n\r\n\/\/ ------------------- TEXT -------------------\r\n\r\nvoid drawChar(uint16_t* buf, int w, int h, int x, int y, char c, uint16_t color) {\r\n\r\nconst uint8_t* chr = nullptr;\r\nif (c &gt;= 32 &amp;&amp; c &lt;= 123)\r\nchr = font5x7[c - 32];\r\nelse\r\nreturn;\r\n\r\nfor (int col = 0; col &lt; 5; col++) {\r\nuint8_t line = chr[col];\r\nfor (int row = 0; row &lt; 7; row++) {\r\nif (line &amp; (1 &lt;&lt; row)) {\r\nint px = x + col;\r\nint py = y + row;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h)\r\nbuf[py * w + px] = color;\r\n}\r\n}\r\n}\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawText(uint16_t* buf, int w, int h, int x, int y, const char* text, uint16_t color) {\r\nwhile (*text) {\r\ndrawChar(buf, w, h, x, y, *text++, color);\r\nx += 6;\r\n}\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawUpperRightText(uint16_t* buf, int w, int h) {\r\nchar UpperRightText[32];\r\n\r\nchar pitchStr[16];\r\ndtostrf(pitch, 6, 2, pitchStr); \/\/ Width: 6, Precision: 2 (e.g., \" 12.34\")\r\nsprintf(UpperRightText, \"Pitch: %s%c\", pitchStr, 123); \/\/ 123 is the index for \u00b0 symbol\r\nint len = strlen(UpperRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, 5, UpperRightText, green); \/\/ Green\r\n\r\n\r\nchar rollStr[16];\r\ndtostrf(roll, 6, 2, rollStr); \/\/ Width: 6, Precision: 2 (e.g., \" 12.34\")\r\nsprintf(UpperRightText, \"Roll: %s%c\", rollStr, 123); \/\/ 123 is the index for \u00b0 symbol\r\nlen = strlen(UpperRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, 14, UpperRightText, green); \/\/ Green\r\n\r\n\r\nchar yawStr[16];\r\n\/\/ Normalize yaw to 0\u2013360 range\r\nfloat displayYaw = yaw;\r\nif (displayYaw &lt; 0) displayYaw += 360;\r\ndtostrf(displayYaw, 6, 2, yawStr); \/\/ Width: 6, Precision: 2\r\nsprintf(UpperRightText, \"Yaw: %s%c\", yawStr, 123); \/\/ 123 is the index for \u00b0 symbol\r\nlen = strlen(UpperRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, 23, UpperRightText, green); \/\/ Green\r\n\r\n\r\nsprintf(UpperRightText, \"Alt: \");\r\nlen = strlen(UpperRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, 32, UpperRightText, green); \/\/ Green\r\n\r\n\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawUpperLeftText(uint16_t* buf, int w, int h) {\r\nchar UpperLeftText[32];\r\nsprintf(UpperLeftText, \"GPS: \");\r\ndrawText(buf, w, h, 5, 5, UpperLeftText, green); \/\/ Green\r\nsprintf(UpperLeftText, \"Date: \");\r\ndrawText(buf, w, h, 5, 14, UpperLeftText, green); \/\/ Green\r\nsprintf(UpperLeftText, \"Time: \");\r\ndrawText(buf, w, h, 5, 23, UpperLeftText, green); \/\/ Green\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawMidLeftText(uint16_t* buf, int w, int h) {\r\nchar MidLeftText[32];\r\nsprintf(MidLeftText, \"Spd: \");\r\ndrawText(buf, w, h, 22, 116, MidLeftText, green); \/\/ Green\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawMidRightText(uint16_t* buf, int w, int h) {\r\nchar MidRightText[32];\r\nsprintf(MidRightText, \"Ele: \");\r\nint len = strlen(MidRightText);\r\ndrawText(buf, w, h, w - len * 6 - 22, 116, MidRightText, green); \/\/ Green\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawLowerLeftText(uint16_t* buf, int w, int h) {\r\nchar LowerLeftText[32];\r\nsprintf(LowerLeftText, \"ESP32-CAM_Video_ver14c\");\r\ndrawText(buf, w, h, 5, h - 21, LowerLeftText, green); \/\/ Green\r\nsprintf(LowerLeftText, \"June 28 2025\");\r\ndrawText(buf, w, h, 5, h - 12, LowerLeftText, green); \/\/ Green\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawLowerRightText(uint16_t* buf, int w, int h) {\r\nchar LowerRightText[32];\r\nunsigned long ms = millis();\r\nunsigned long total_seconds = ms \/ 1000;\r\nunsigned long hours = total_seconds \/ 3600;\r\nunsigned long minutes = (total_seconds % 3600) \/ 60;\r\nunsigned long seconds = total_seconds % 60;\r\nunsigned long tenths = (ms % 1000) \/ 100; \/\/ Tenths of a second\r\n\/\/ Format as \"HH:MM:SS.S\"\r\nsprintf(LowerRightText, \"Uptime: %02lu:%02lu:%02lu.%1lu\", hours, minutes, seconds, tenths);\r\nint len = strlen(LowerRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, h - 12, LowerRightText, 0x07E0); \/\/ Green\r\n\r\n\r\n\/\/ Use Pitch values for temperature in degrees C\r\nchar pitchStr[16];\r\ndtostrf(pitch, 6, 2, pitchStr); \/\/ Width: 6, Precision: 2 (e.g., \" 12.34\")\r\nsprintf(LowerRightText, \"Temp: %s%c C\", pitchStr, 123); \/\/ 123 is the index for \u00b0 symbol\r\nlen = strlen(LowerRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, h - 21, LowerRightText, green); \/\/ Green\r\n\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- JPEG SAVE -------------------\r\nvoid saveAsJPEG(uint16_t* rgb565_buf, int w, int h, const char *path) {\r\nFile file = SD_MMC.open(path, FILE_WRITE);\r\nif (!file) {\r\nreturn;\r\n}\r\n\r\n\/\/ Convert RGB565 to RGB888\r\nuint8_t *rgb888_buf = (uint8_t*)ps_malloc(w * h * 3);\r\nif (!rgb888_buf) {\r\nreturn;\r\n}\r\n\r\nfor (int i = 0; i &lt; w * h; i++) {\r\nuint16_t pixel = rgb565_buf[i];\r\nuint8_t r = ((pixel &gt;&gt; 11) &amp; 0x1F) * 255 \/ 31;\r\nuint8_t g = ((pixel &gt;&gt; 5) &amp; 0x3F) * 255 \/ 63;\r\nuint8_t b = (pixel &amp; 0x1F) * 255 \/ 31;\r\n\r\nrgb888_buf[i * 3 + 0] = r;\r\nrgb888_buf[i * 3 + 1] = g;\r\nrgb888_buf[i * 3 + 2] = b;\r\n}\r\n\r\nuint8_t *jpg_buf = NULL;\r\nsize_t jpg_len = 0;\r\n\r\n\/\/ JPEG Quality at 100%\r\nif (!fmt2jpg(rgb888_buf, w * h * 3, w, h, PIXFORMAT_RGB888, 100, &amp;jpg_buf, &amp;jpg_len)) {\r\nfree(rgb888_buf);\r\nfile.close();\r\nreturn;\r\n}\r\n\r\nfile.write(jpg_buf, jpg_len);\r\nfile.close();\r\nfree(jpg_buf);\r\nfree(rgb888_buf);\r\n}<\/pre>\n<p>This writes a series of images to the microSD media which I then used this script to create a video using FFMpeg.<\/p>\n<pre>#!\/bin\/bash\r\n\r\n# Directory containing image frames\r\nINPUT_DIR=\".\/microSD\" # &lt;-- Change to your desired folder\r\nOSD_VIDEO=\"ESP32-Cam_HUD-Demo.mp4\"\r\nFRAMERATE=30 # Set desired FPS for the output video\r\n\r\n# Check if the input directory exists\r\nif [ ! -d \"$INPUT_DIR\" ]; then\r\necho \"Error: Directory $INPUT_DIR not found.\"\r\nexit 1\r\nfi\r\n\r\n# Create video from ordered image sequence\r\nffmpeg -framerate $FRAMERATE -pattern_type glob -i \"${INPUT_DIR}\/frame_sim_*.jpg\" \\\r\n-c:v libx264 -pix_fmt yuv420p -crf 23 -preset medium \"$OSD_VIDEO\"\r\n\r\necho \"Video saved to $OSD_VIDEO\"<\/pre>\n<p>If you&#8217;re still here, then here is the code to generate the text and color pattern during my debug.\u00a0 I used a modified version of the FFMpeg script above to make a video out of the image sequence.<\/p>\n<pre>\/*\r\n\r\nESP32-CAM_Video_ver15\r\n\r\nThis program captures a text pattern to test font characters and colors\r\nThe code was developed using ChatGPT\r\n\r\nEdited by Patrick Gilfeather June 28 2025\r\n\r\nSoftware used\r\n- Arduino IDE version 1.8.19\r\n- ESP32 core version 2.0.4 \r\n- Board AI Thinker ESP32-CAM\r\n- Partition Scheme Huge APP (3MB No OTA)\r\n\r\nHardware used\r\n- Espressif ESP32-Cam module\r\n- SanDisk Ultra 64GB microSD class 10 format FAT (32-bit version)\r\n\r\n*\/\r\n\r\n#include \"FS.h\"\r\n#include \"SD_MMC.h\"\r\n#include \"esp_camera.h\"\r\n#include \"img_converters.h\"\r\n\r\n#define FRAME_WIDTH 320\r\n#define FRAME_HEIGHT 240\r\n\r\nuint32_t frameCounter = 0;\r\n\r\n#define RGB565(r8, g8, b8) ( ((r8 &amp; 0xF8) &lt;&lt; 8) | ((g8 &amp; 0xFC) &lt;&lt; 3) | (b8 &gt;&gt; 3) )\r\n\r\n\/\/ The format follows this (blue, green, red)\r\nuint16_t black = RGB565(0, 0, 0); \/\/ Black\r\nuint16_t white = RGB565(255, 255, 255); \/\/ White\r\nuint16_t blue = RGB565(255, 0, 0); \/\/ Blue\r\nuint16_t green = RGB565(0, 255, 0); \/\/ Green\r\nuint16_t red = RGB565(0, 0, 255); \/\/ Red\r\nuint16_t yellow = RGB565(0, 255, 255); \/\/ Yellow\r\nuint16_t cyan = RGB565(255, 255, 0); \/\/ Cyan\r\nuint16_t magenta = RGB565(255, 0, 255); \/\/ Magenta\r\nuint16_t orange = RGB565(0, 128, 255); \/\/ Orange\r\nuint16_t brown = RGB565(32, 32, 128); \/\/ Brown\r\nuint16_t gray = RGB565(128, 128, 128); \/\/ Gray\r\n\r\nstatic const uint8_t font5x7[][5] = { \/\/ ASCII 32 to 123 (printable characters) \r\n{0x00,0x00,0x00,0x00,0x00}, \/\/ = 32\r\n{0x00,0x00,0x5F,0x00,0x00}, \/\/ ! = 33\r\n{0x00,0x07,0x00,0x07,0x00}, \/\/ \" = 34\r\n{0x14,0x7F,0x14,0x7F,0x14}, \/\/ # = 35 \r\n{0x24,0x2A,0x7F,0x2A,0x12}, \/\/ $ = 36\r\n{0x23,0x13,0x08,0x64,0x62}, \/\/ % = 37\r\n{0x36,0x49,0x55,0x22,0x50}, \/\/ &amp; = 38\r\n{0x00,0x05,0x03,0x00,0x00}, \/\/ ' = 39 \r\n{0x00,0x1C,0x22,0x41,0x00}, \/\/ ( = 40\r\n{0x00,0x41,0x22,0x1C,0x00}, \/\/ ) = 41\r\n{0x14,0x08,0x3E,0x08,0x14}, \/\/ * = 42\r\n{0x08,0x08,0x3E,0x08,0x08}, \/\/ + = 43 \r\n{0x00,0x50,0x30,0x00,0x00}, \/\/ , = 44\r\n{0x08,0x08,0x08,0x08,0x08}, \/\/ - = 45\r\n{0x00,0x60,0x60,0x00,0x00}, \/\/ . = 46\r\n{0x20,0x10,0x08,0x04,0x02}, \/\/ \/ = 47 \r\n{0x3E,0x51,0x49,0x45,0x3E}, \/\/ 0 = 48\r\n{0x00,0x42,0x7F,0x40,0x00}, \/\/ 1 = 49\r\n{0x42,0x61,0x51,0x49,0x46}, \/\/ 2 = 50\r\n{0x21,0x41,0x45,0x4B,0x31}, \/\/ 3 = 51 \r\n{0x18,0x14,0x12,0x7F,0x10}, \/\/ 4 = 52\r\n{0x27,0x45,0x45,0x45,0x39}, \/\/ 5 = 53\r\n{0x3C,0x4A,0x49,0x49,0x30}, \/\/ 6 = 54\r\n{0x01,0x71,0x09,0x05,0x03}, \/\/ 7 = 55 \r\n{0x36,0x49,0x49,0x49,0x36}, \/\/ 8 = 56\r\n{0x06,0x49,0x49,0x29,0x1E}, \/\/ 9 = 57\r\n{0x00,0x36,0x36,0x00,0x00}, \/\/ : = 58\r\n{0x00,0x56,0x36,0x00,0x00}, \/\/ ; = 59 \r\n{0x08,0x14,0x22,0x41,0x00}, \/\/ &lt; = 60\r\n{0x14,0x14,0x14,0x14,0x14}, \/\/ = = 61\r\n{0x00,0x41,0x22,0x14,0x08}, \/\/ &gt; = 62\r\n{0x02,0x01,0x51,0x09,0x06}, \/\/ ? = 63 \r\n{0x32,0x49,0x79,0x41,0x3E}, \/\/ @ = 64\r\n{0x7E,0x11,0x11,0x11,0x7E}, \/\/ A = 65\r\n{0x7F,0x49,0x49,0x49,0x36}, \/\/ B = 66\r\n{0x3E,0x41,0x41,0x41,0x22}, \/\/ C = 67 \r\n{0x7F,0x41,0x41,0x22,0x1C}, \/\/ D = 68\r\n{0x7F,0x49,0x49,0x49,0x41}, \/\/ E = 69\r\n{0x7F,0x09,0x09,0x09,0x01}, \/\/ F = 70\r\n{0x3E,0x41,0x49,0x49,0x7A}, \/\/ G = 71 \r\n{0x7F,0x08,0x08,0x08,0x7F}, \/\/ H = 72\r\n{0x00,0x41,0x7F,0x41,0x00}, \/\/ I = 73\r\n{0x20,0x40,0x41,0x3F,0x01}, \/\/ J = 74\r\n{0x7F,0x08,0x14,0x22,0x41}, \/\/ K = 75 \r\n{0x7F,0x40,0x40,0x40,0x40}, \/\/ L = 76\r\n{0x7F,0x02,0x0C,0x02,0x7F}, \/\/ M = 77\r\n{0x7F,0x04,0x08,0x10,0x7F}, \/\/ N = 78\r\n{0x3E,0x41,0x41,0x41,0x3E}, \/\/ O = 79 \r\n{0x7F,0x09,0x09,0x09,0x06}, \/\/ P = 80\r\n{0x3E,0x41,0x51,0x21,0x5E}, \/\/ Q = 81\r\n{0x7F,0x09,0x19,0x29,0x46}, \/\/ R = 82\r\n{0x46,0x49,0x49,0x49,0x31}, \/\/ S = 83 \r\n{0x01,0x01,0x7F,0x01,0x01}, \/\/ T = 84\r\n{0x3F,0x40,0x40,0x40,0x3F}, \/\/ U = 85\r\n{0x1F,0x20,0x40,0x20,0x1F}, \/\/ V = 86\r\n{0x3F,0x40,0x38,0x40,0x3F}, \/\/ W = 87 \r\n{0x63,0x14,0x08,0x14,0x63}, \/\/ X = 88\r\n{0x07,0x08,0x70,0x08,0x07}, \/\/ Y = 89\r\n{0x61,0x51,0x49,0x45,0x43}, \/\/ Z = 90\r\n{0x00,0x7F,0x41,0x41,0x00}, \/\/ [ = 91 \r\n{0x02,0x04,0x08,0x10,0x20}, \/\/ \\ = 92\r\n{0x00,0x41,0x41,0x7F,0x00}, \/\/ ] = 93\r\n{0x04,0x02,0x01,0x02,0x04}, \/\/ ^ = 94\r\n{0x40,0x40,0x40,0x40,0x40}, \/\/ _ = 95 \r\n{0x00,0x01,0x02,0x04,0x00}, \/\/ ` = 96\r\n{0x20,0x54,0x54,0x54,0x78}, \/\/ a = 97\r\n{0x7F,0x48,0x44,0x44,0x38}, \/\/ b = 98\r\n{0x38,0x44,0x44,0x44,0x20}, \/\/ c = 99 \r\n{0x38,0x44,0x44,0x48,0x7F}, \/\/ d = 100\r\n{0x38,0x54,0x54,0x54,0x18}, \/\/ e = 101\r\n{0x08,0x7E,0x09,0x01,0x02}, \/\/ f = 102\r\n{0x0C,0x52,0x52,0x52,0x3E}, \/\/ g = 103 \r\n{0x7F,0x08,0x04,0x04,0x78}, \/\/ h = 104\r\n{0x00,0x44,0x7D,0x40,0x00}, \/\/ i = 105\r\n{0x20,0x40,0x44,0x3D,0x00}, \/\/ j = 106\r\n{0x7F,0x10,0x28,0x44,0x00}, \/\/ k = 107 \r\n{0x00,0x41,0x7F,0x40,0x00}, \/\/ l = 108\r\n{0x7C,0x04,0x18,0x04,0x78}, \/\/ m = 109\r\n{0x7C,0x08,0x04,0x04,0x78}, \/\/ n = 110\r\n{0x38,0x44,0x44,0x44,0x38}, \/\/ o = 111 \r\n{0x7C,0x14,0x14,0x14,0x08}, \/\/ p = 112\r\n{0x08,0x14,0x14,0x18,0x7C}, \/\/ q = 113\r\n{0x7C,0x08,0x04,0x04,0x08}, \/\/ r = 114\r\n{0x48,0x54,0x54,0x54,0x20}, \/\/ s = 115 \r\n{0x04,0x3F,0x44,0x40,0x20}, \/\/ t = 116\r\n{0x3C,0x40,0x40,0x20,0x7C}, \/\/ u = 117\r\n{0x1C,0x20,0x40,0x20,0x1C}, \/\/ v = 118\r\n{0x3C,0x40,0x30,0x40,0x3C}, \/\/ w = 119 \r\n{0x44,0x28,0x10,0x28,0x44}, \/\/ x = 120\r\n{0x0C,0x50,0x50,0x50,0x3C}, \/\/ y = 121\r\n{0x44,0x64,0x54,0x4C,0x44}, \/\/ z = 122\r\n{0x06,0x09,0x09,0x06,0x00} \/\/ \u00b0 = 123\r\n};\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- SETUP -------------------\r\nvoid setup() {\r\ndelay(10);\r\n\r\nif (!SD_MMC.begin()) {\r\nreturn;\r\n}\r\n\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- LOOP -------------------\r\nvoid loop() {\r\n\r\nuint16_t* buffer = createBlackFrameRGB565();\r\nif (!buffer) return;\r\n\r\ndrawAllCharacters(buffer, FRAME_WIDTH, FRAME_HEIGHT, frameCounter);\r\ndrawLowerLeftText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\ndrawLowerRightText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\n\r\nchar filename[32];\r\nsprintf(filename, \"\/frame_sim_%05lu.jpg\", frameCounter);\r\nsaveAsJPEG(buffer, FRAME_WIDTH, FRAME_HEIGHT, filename);\r\n\r\nframeCounter++;\r\n\r\nfree(buffer);\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- FRAME CREATION -------------------\r\nuint16_t* createBlackFrameRGB565() {\r\nsize_t bufferSize = FRAME_WIDTH * FRAME_HEIGHT * sizeof(uint16_t);\r\nuint16_t* buffer = (uint16_t*)ps_malloc(bufferSize);\r\nif (!buffer) {\r\nreturn nullptr;\r\n}\r\nmemset(buffer, 0x00, bufferSize);\r\nreturn buffer;\r\n}\r\n\r\n\r\n\r\n\/\/ ------------------- TEXT -------------------\r\nvoid drawChar(uint16_t* buf, int w, int h, int x, int y, char c, uint16_t color) {\r\n\r\nconst uint8_t* chr = nullptr;\r\nif (c &gt;= 32 &amp;&amp; c &lt;= 123)\r\nchr = font5x7[c - 32];\r\nelse\r\nreturn;\r\n\r\nfor (int col = 0; col &lt; 5; col++) {\r\nuint8_t line = chr[col];\r\nfor (int row = 0; row &lt; 7; row++) {\r\nif (line &amp; (1 &lt;&lt; row)) {\r\nint px = x + col;\r\nint py = y + row;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h)\r\nbuf[py * w + px] = color;\r\n}\r\n}\r\n}\r\n}\r\n\r\nvoid drawAllCharacters(uint16_t* buf, int w, int h, int startCharOffset) {\r\nconst int startX = 5;\r\nconst int startY = 5;\r\nconst int charWidth = 6; \/\/ 5 pixels + 1 spacing\r\nconst int lineHeight = 9; \/\/ 7 pixels + 2 spacing\r\n\r\nuint16_t colors[] = { blue, cyan, white, yellow, orange, red, gray, brown, green, magenta };\r\nconst int numColors = sizeof(colors) \/ sizeof(colors[0]);\r\n\r\nconst int firstChar = 32;\r\nconst int lastChar = 123;\r\nconst int numChars = lastChar - firstChar + 1;\r\n\r\n\/\/ Clamp startCharOffset into valid range\r\nstartCharOffset %= numChars;\r\n\r\nint x = startX;\r\nint y = startY;\r\n\r\n\/\/ Full sweep through colors\r\nfor (int i = 0; i &lt; numColors; i++) {\r\n\r\n\/\/ Draw from startCharOffset .. lastChar\r\nfor (int j = startCharOffset; j &lt; numChars; j++) {\r\nint c = firstChar + j;\r\ndrawChar(buf, w, h, x, y, c, colors[i]);\r\n\r\nx += charWidth;\r\nif (x + charWidth &gt; w) {\r\nx = startX;\r\ny += lineHeight;\r\nif (y + 7 &gt; h) return; \/\/ Stop if we run out of space\r\n}\r\n}\r\n\r\n\/\/ Draw from firstChar .. startCharOffset-1\r\nfor (int j = 0; j &lt; startCharOffset; j++) {\r\nint c = firstChar + j;\r\ndrawChar(buf, w, h, x, y, c, colors[i]);\r\n\r\nx += charWidth;\r\nif (x + charWidth &gt; w) {\r\nx = startX;\r\ny += lineHeight;\r\nif (y + 7 &gt; h) return; \/\/ Stop if we run out of space\r\n}\r\n}\r\n}\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawText(uint16_t* buf, int w, int h, int x, int y, const char* text, uint16_t color) {\r\nwhile (*text) {\r\ndrawChar(buf, w, h, x, y, *text++, color);\r\nx += 6;\r\n}\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawLowerLeftText(uint16_t* buf, int w, int h) {\r\nchar LowerLeftText[32];\r\nsprintf(LowerLeftText, \"ESP32-CAM_Video_ver15\");\r\ndrawText(buf, w, h, 5, h - 21, LowerLeftText, white);\r\nsprintf(LowerLeftText, \"June 28 2025\");\r\ndrawText(buf, w, h, 5, h - 12, LowerLeftText, white);\r\n}\r\n\r\n\r\n\r\n\r\nvoid drawLowerRightText(uint16_t* buf, int w, int h) {\r\nchar LowerRightText[32];\r\nunsigned long ms = millis();\r\nunsigned long total_seconds = ms \/ 1000;\r\nunsigned long hours = total_seconds \/ 3600;\r\nunsigned long minutes = (total_seconds % 3600) \/ 60;\r\nunsigned long seconds = total_seconds % 60;\r\nunsigned long tenths = (ms % 1000) \/ 100; \/\/ Tenths of a second\r\n\/\/ Format as \"HH:MM:SS.S\"\r\nsprintf(LowerRightText, \"Uptime: %02lu:%02lu:%02lu.%1lu\", hours, minutes, seconds, tenths);\r\nint len = strlen(LowerRightText);\r\ndrawText(buf, w, h, w - len * 6 - 5, h - 12, LowerRightText, white); \/\/ \r\n\r\n\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ ------------------- JPEG SAVE -------------------\r\nvoid saveAsJPEG(uint16_t* rgb565_buf, int w, int h, const char *path) {\r\nFile file = SD_MMC.open(path, FILE_WRITE);\r\nif (!file) {\r\nreturn;\r\n}\r\n\r\n\/\/ Convert RGB565 to RGB888\r\nuint8_t *rgb888_buf = (uint8_t*)ps_malloc(w * h * 3);\r\nif (!rgb888_buf) {\r\nreturn;\r\n}\r\n\r\nfor (int i = 0; i &lt; w * h; i++) {\r\nuint16_t pixel = rgb565_buf[i];\r\nuint8_t r = ((pixel &gt;&gt; 11) &amp; 0x1F) * 255 \/ 31;\r\nuint8_t g = ((pixel &gt;&gt; 5) &amp; 0x3F) * 255 \/ 63;\r\nuint8_t b = (pixel &amp; 0x1F) * 255 \/ 31;\r\n\r\nrgb888_buf[i * 3 + 0] = r;\r\nrgb888_buf[i * 3 + 1] = g;\r\nrgb888_buf[i * 3 + 2] = b;\r\n}\r\n\r\nuint8_t *jpg_buf = NULL;\r\nsize_t jpg_len = 0;\r\n\r\n\/\/ JPG Quality 100%\r\nif (!fmt2jpg(rgb888_buf, w * h * 3, w, h, PIXFORMAT_RGB888, 100, &amp;jpg_buf, &amp;jpg_len)) {\r\nfree(rgb888_buf);\r\nfile.close();\r\nreturn;\r\n}\r\n\r\nfile.write(jpg_buf, jpg_len);\r\nfile.close();\r\nfree(jpg_buf);\r\nfree(rgb888_buf);\r\n}\r\n\r\n<\/pre>\n<p>If you think AI is going to spoon feed you the answer, you&#8217;re in for a scare if you have a deadline looming over your head.<\/p>\n<p><iframe loading=\"lazy\" title=\"C.H.U.D. - Chuddy waters\" width=\"640\" height=\"480\" src=\"https:\/\/www.youtube.com\/embed\/wFvT1qADj9Q?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post is a continuation from an earlier post that introduced the concept of writing pixel data to the ESP32-Cam module frame buffer.\u00a0 The buffer can be streamed or written as an image file to the microSD media.\u00a0 In this post I&#8217;ll expand on text and graphics with a HUD styled overlay.\u00a0 I&#8217;ll be sticking with saving images sequences and not covering streaming here. The code in this post was generated and refined using OpenAI&#8217;s ChatGPT.\u00a0 I won&#8217;t provide the&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4907\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4907","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4907","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4907"}],"version-history":[{"count":13,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4907\/revisions"}],"predecessor-version":[{"id":4920,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4907\/revisions\/4920"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4907"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4907"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4907"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}