Overview
You: an 80s-style cyberpunk hacker who's got to spread your video message to the people. Jam this tiny, battery powered video nub shank into a state-of-the-art composite video input of a "television" and run!
Use the QT Py ESP32 Pico to build a stylish vaporwave clock, NTSC SPMTE color bar test pattern generator, or other lo-fi/hi-style video applications.
Thanks to the incredible coding efforts of rossumur, bitluni and marciot, it's possible to generate an analog composite video signal with the original ESP32 chip's 8-bit DACs.
or, alternatively, use a jack and cable instead of a plug:
You'll need a TV or monitor with a composite video input -- this is nearly always a yellow RCA jack.
Some modern displays have compositve video inputs, but this looks particularly awesome on an old CRT TV!
Page last edited March 08, 2024
Text editor powered by tinymce.
Build the Video Nub Shank
The most basic setup is to simply wire the QT Py GND to the negative (sleeve) conductor of the video plug, and the QT Py A0 pin to positive (tip), as shown below.
To get a bit fancier, you can create a small sandwich of QT Py, video plug, and BFF charger board, with a side of LiPo battery. This way, you'll be able to run from battery, or plug into USB C for power and charging.
You can choose to use the plug or jack version of the video terminal block breakout. These can also be swapped later.
Or, get a socket-socket adapter to allow the nub to be used in cable mode.
Assemble Headers, Wires
Solder the extra long header pins to the QT Py as shown -- Note, the addition of a second set of spacers is highly optional, it only serves to look cool.
Solder the header sockets to the underside of the BFF.
Solder a short black wire from the QT Py GND to the composite video plug's negative (-) terminal and a yellow wire from the QT Py A0 to the video plug's positive (+) terminal as shown.
Then, sandwich the plug between the boards as shown. Delicious.
Add Battery
Plug the battery into the BFF.
Use a small loop of Kapton tape or a small piece of double-sided foam tape to secure the battery to the header.
Inject Video
You can plug right into the composite video jack of a TV or monitor, flip the BFF power switch to on, and set the input to "composite" or "video".
USB Powered
If your monitor has a USB port you may be able to power (and charge) the video nub shank using a short USB C to USB A cable.
Page last edited March 08, 2024
Text editor powered by tinymce.
Program the Video Nub Shank
One excellent demo for video on ESP32 is the esp32-dali-clock by marciot. This is an Arduino sketch and supporting libraries, so make sure you have the Arduino IDE installed and can successfully run the Blink sketch before proceeding. This guide shows how, but is for the newer ESP32-S2 -- we'll update this once the QT Py ESP32 Pico guide is ready.
Download
To run this demo, first download the .zip file and uncompress it.
Move the esp32-dali-clock folder into your Arduino sketches directory.
Settings, Compile, Upload
In order to compile and upload to the board, first select Tools > Board > ESP32 Arduino > Adafruit QT Py ESP32.
Then, match the other settings shown here. Very important: Click Tools > PSRAM > Disabled. If you don't disable the PSRAM the video will suffer vertical sync issues and jiggle up and down!
When you're ready, click Sketch > Upload (or use the upload button) to compile and upload the code to the board.
The board will reset after upload and begin immediately running the code. Plug the nub into a composite video input on your TV or monitor and you'll see the awesome vaporwave clock.
Set the Clock
The ESP32's built in WiFi makes it possible to host a small web server in order to configure the clock.
On your computer or mobile device, choose the "ESP32 Dali Clock" network. You'll be taken to a setup page where you can configure the graphics style, date/time, and local network settings.
You can even set the color theme to a specific choice or allow it to change based on the time of day.
Page last edited March 08, 2024
Text editor powered by tinymce.
NTSC Test Pattern Generator
Another nifty use for the Video Nub Shank is to create a test pattern generator for a composite TV or display.
Ladyada tests out little displays we're building, and wanted a way to easily check the geometry, rotation, orientation, and colors.
I created a SPMTE NTSC color bar test pattern in code using the Adafruit GFX library running the ESP_8_BIT_composite library by Roger Cheng. While the colors are not totally accurate to SMPTE standards due to the RGB332 bit depth used, it's close enough to do the job.
Also, it looks really rad on an old CRT.
Install Library and Code
To run the Test Pattern Generator, you'll need to download the ESP_8_BIT_composite library, uncompress the .zip, and then move the folder to your Arduino libraries directory.
Rename the folder from ESP_8_BIT_composite-master to ESP_8_BIT_composite.
In the Arduino IDE, open the File > Examples > ESP_8_BIT Color Composite Video Library > SMPTE_NTSC_Color_Bars.ino file.
Settings, Compile, Upload
In order to compile and upload to the board, first select Tools > Board > ESP32 Arduino > Adafruit QT Py ESP32.
Then, match the other settings shown here. Very important: Click Tools > PSRAM > Disabled. If you don't disable the PSRAM the video will suffer vertical sync issues and jiggle up and down!
When you're ready, click Sketch > Upload (or use the upload button) to compile and upload the code to the board.
/*
Adafruit NTSC SMPTE color bars
copyright: 2022 John Park for Adafruit Industries
License: MIT
resolution is 256x224, colors are approximate at best
uses composite video generator library on ESP32 by Roger Cheng
Connect GPIO25 (A0 on QT Py ESP32 Pico) to signal line, usually the center of composite video plug.
Please disable PSRAM option before uploading to the board, otherwise there may be vertical jitter.
*/
#include <ESP_8_BIT_GFX.h>
// Create an instance of the graphics library
ESP_8_BIT_GFX videoOut(true /* = NTSC */, 8 /* = RGB332 color */);
uint8_t WHITE = 0xFF ;
uint8_t DIM_WHITE = 0xB6 ;
uint8_t YELLOW = 0xF4 ;
uint8_t TEAL = 0x1C ;
uint8_t GREEN = 0x70 ;
uint8_t MAGENTA = 0x83 ;
uint8_t RED = 0x82 ;
uint8_t BLUE = 0x0B ;
uint8_t DARK_BLUE = 0x06 ;
uint8_t PURPLE = 0x23 ;
uint8_t BLACK = 0x00 ;
uint8_t GRAY = 0x24 ;
uint8_t LIGHT_GRAY = 0x29 ;
uint8_t height_tall = 149 ;
uint8_t height_squat = 19 ;
uint8_t height_med = 56 ;
uint8_t width_med = 36 ;
uint8_t width_large = 46;
uint8_t width_skinny = 12 ;
uint8_t row2_y = height_tall ;
uint8_t row3_y = height_tall + height_squat ;
void setup() {
// Initial setup of graphics library
videoOut.begin();
}
void loop() {
// Wait for the next frame to minimize chance of visible tearing
videoOut.waitForFrame();
// Clear screen
videoOut.fillScreen(0);
// Draw rectangles
//row 1
videoOut.fillRect(0, 0, width_med, height_tall, DIM_WHITE);
videoOut.fillRect(width_med, 0, width_med, height_tall, YELLOW);
videoOut.fillRect(width_med*2, 0, width_med, height_tall, TEAL);
videoOut.fillRect(width_med*3, 0, width_med, height_tall, GREEN);
videoOut.fillRect(width_med*4, 0, width_med, height_tall, MAGENTA);
videoOut.fillRect(width_med*5, 0, width_med, height_tall, RED);
videoOut.fillRect(width_med*6, 0, width_med, height_tall, BLUE);
//row 2
videoOut.fillRect(0, row2_y, width_med, height_squat, BLUE);
videoOut.fillRect(width_med, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*2, row2_y, width_med, height_squat, MAGENTA);
videoOut.fillRect(width_med*3, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*4, row2_y, width_med, height_squat, TEAL);
videoOut.fillRect(width_med*5, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*6, row2_y , width_med, height_squat, DIM_WHITE);
//row 3
videoOut.fillRect(0, row3_y, width_large, height_med, DARK_BLUE);
videoOut.fillRect(width_large, row3_y, width_large, height_med, WHITE);
videoOut.fillRect(width_large*2, row3_y, width_large, height_med, PURPLE);
videoOut.fillRect(width_large*3, row3_y, width_large, height_med, GRAY);
videoOut.fillRect(width_large*4, row3_y, width_skinny, height_med, BLACK);
videoOut.fillRect(width_large*4+width_skinny, row3_y, width_skinny, height_med, GRAY);
videoOut.fillRect(((width_large*4)+(width_skinny*2)), row3_y , width_skinny, height_med, LIGHT_GRAY);
videoOut.fillRect(width_med*6, row3_y , width_med, height_med, GRAY);
// Draw text
videoOut.setCursor(144, 180);
videoOut.setTextColor(0xFF);
videoOut.print("Adafruit NTSC");
videoOut.setCursor(144, 190);
videoOut.setTextColor(0xFF);
videoOut.print("composite video");
}
How it Works
The ESP_8_BIT_composite library makes it easy to use the Adafruit GFX library commands to display images on screen.
videoOut Creation
First, you'll import the library and create and instance of ESP_8_BIT_GFX called videoOut. The true argument chooses NTSC (vs. PAL), and the 8 chooses 8-bit RGB332 color.
#include <ESP_8_BIT_GFX.h> // Create an instance of the graphics library ESP_8_BIT_GFX videoOut(true /* = NTSC */, 8 /* = RGB332 color */);
Colors and Sizes
Next, a number of colors are defined to approximate the NTSC color bars. This was accomplished with the incredibly cool 3D color picker Roger Cheng created here, which outputs hex values for RGB332 color space.
Colors and Sizes
Next, a number of colors are defined to approximate the NTSC color bars. This was accomplished with the incredibly cool 3D color picker Roger Cheng created here, which outputs hex values for RGB332 color space.
The pixel dimensions are defined for the different sizes of bars.
uint8_t WHITE = 0xFF ; uint8_t DIM_WHITE = 0xB6 ; uint8_t YELLOW = 0xF4 ; uint8_t TEAL = 0x1C ; uint8_t GREEN = 0x70 ; uint8_t MAGENTA = 0x83 ; uint8_t RED = 0x82 ; uint8_t BLUE = 0x0B ; uint8_t DARK_BLUE = 0x06 ; uint8_t PURPLE = 0x23 ; uint8_t BLACK = 0x00 ; uint8_t GRAY = 0x24 ; uint8_t LIGHT_GRAY = 0x29 ; uint8_t height_tall = 149 ; uint8_t height_squat = 19 ; uint8_t height_med = 56 ; uint8_t width_med = 36 ; uint8_t width_large = 46; uint8_t width_skinny = 12 ; uint8_t row2_y = height_tall ; uint8_t row3_y = height_tall + height_squat ;
void setup() {
// Initial setup of graphics library
videoOut.begin();
}
Main Loop
In the main loop, the library waits for a frame, then clears the screen, and draws the bars, based on some procedural rules.
// Wait for the next frame to minimize chance of visible tearing
videoOut.waitForFrame();
// Clear screen
videoOut.fillScreen(0);
// Draw rectangles
//row 1
videoOut.fillRect(0, 0, width_med, height_tall, DIM_WHITE);
videoOut.fillRect(width_med, 0, width_med, height_tall, YELLOW);
videoOut.fillRect(width_med*2, 0, width_med, height_tall, TEAL);
videoOut.fillRect(width_med*3, 0, width_med, height_tall, GREEN);
videoOut.fillRect(width_med*4, 0, width_med, height_tall, MAGENTA);
videoOut.fillRect(width_med*5, 0, width_med, height_tall, RED);
videoOut.fillRect(width_med*6, 0, width_med, height_tall, BLUE);
//row 2
videoOut.fillRect(0, row2_y, width_med, height_squat, BLUE);
videoOut.fillRect(width_med, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*2, row2_y, width_med, height_squat, MAGENTA);
videoOut.fillRect(width_med*3, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*4, row2_y, width_med, height_squat, TEAL);
videoOut.fillRect(width_med*5, row2_y, width_med, height_squat, GRAY);
videoOut.fillRect(width_med*6, row2_y , width_med, height_squat, DIM_WHITE);
//row 3
videoOut.fillRect(0, row3_y, width_large, height_med, DARK_BLUE);
videoOut.fillRect(width_large, row3_y, width_large, height_med, WHITE);
videoOut.fillRect(width_large*2, row3_y, width_large, height_med, PURPLE);
videoOut.fillRect(width_large*3, row3_y, width_large, height_med, GRAY);
videoOut.fillRect(width_large*4, row3_y, width_skinny, height_med, BLACK);
videoOut.fillRect(width_large*4+width_skinny, row3_y, width_skinny, height_med, GRAY);
videoOut.fillRect(((width_large*4)+(width_skinny*2)), row3_y , width_skinny, height_med, LIGHT_GRAY);
videoOut.fillRect(width_med*6, row3_y , width_med, height_med, GRAY);
Text
In order to make it simple to verify that a display is showing the video in the proper orientation/rotation a small bit of text is printed to the screen. You can adjust this text to suit your needs.
// Draw text
videoOut.setCursor(144, 180);
videoOut.setTextColor(0xFF);
videoOut.print("Adafruit NTSC");
videoOut.setCursor(144, 190);
videoOut.setTextColor(0xFF);
videoOut.print("composite video");
Page last edited March 08, 2024
Text editor powered by tinymce.