Hexagons are cool -- they have six sides, they tile a plane, they have a structure like the carbon atoms in graphite, each cell has six neighbors, and there are six cardinal directions. But mostly, they are not square when so many computer things are. So I wanted to make a project using a display made of hexagonal pixels (hexels). I wanted the display to do something useful in addition to looking interesting, so I made it a clock.
The clock has 13 columns with either four or five LEDs, a total of 58 hexels that mostly fills a 17"x10" picture frame. It runs standalone keeping time via a battery-backup real-time clock, or can be connected via USB to a host computer that can push bitmaps, change color schemes, or most importantly set the internal clock. It definitely has some quirks, but I have learned to enjoy them.
Just for fun, it makes a transition at the end of each minute, which is sometimes an appreciation of John Conway's "Game of Life". For more information on Conway, see this article in the NYTimes or enjoy this cartoon by XKCD.
- Addressable LED strip (Adafruit NeoPixels, 2 meters of 30 pixels per meter, https://www.adafruit.com/product/1460)
- Microcontroller (Adafruit Metro Mini 328, https://www.adafruit.com/product/2590)
- Shadow-box picture frame. I used a 17"x10" picture frame meant to display three 4"x6" photos. The dimensions are the area of the glass. The frame needs some distance (~1/2" for my frame) between the glass and back plate. I used "Gallery Collage Photo Frame 3 Image 4''x6'' Black" frame that I got at JoAnn Fabrics (https://www.joann.com/gallery-collage-photo-frame-3-image-4x6-black/16619298.html)
- Smoke-tinted window cling film (I used Gila Smoke Glare Control film) -- or smoked plexiglas
- Battery-backup real time clock (a DS3231 module I got on Amazon)
- (optional) Light sensor (Adafruit VEML7700 Lux Sensor, https://www.adafruit.com/product/4162)
- 100 microfarad (or larger) electrolytic capacitor
- 220 ohm Resistor
- Wires, solder, soldering iron
- USB cord for power/communications
Step 1: Plan and Make a Font
The basic plan was to make a hexagonal grid of NeoPixel LEDs and then display the time on it. I found an INCREDIBLE website describing hexagonal coordinate systems (https://www.redblobgames.com/grids/hexagons/) that really helped with the design, and we'll refer to this site often. If you want to play hexagonal life, there is also a nice interactive site at https://arunarjunakani.github.io/HexagonalGameOfLife/.
I chose to use a "flat topped" grid because I could make a font that was fairly readable, but maintained a significantly hexagonal look. A couple digits are a bit of a stretch (4, 5, and 7), but you get used to them. I also found I could make all hexadecimal digits (0..F), which I thought was in agreement with the hex idea, so I adopted this font.
The font uses 13 hexels, three columns, and 4/5/4 rows. I encoded this font into a bitmap by using 16-bit unsigned integers and created bitmaps for each digit, which are in the Arduino code that you can download from the GitHub link.
Step 2: Make the Grid and Coordinates
After making the font decision, I needed to figure out how big the grid would be. In addition to the three-column-wide digit, we need an extra column for the space between digits, so that means four columns per digit and we need three full digits for the clock (12 columns), and then a leading 1 digit, which would be made of a single four-high row. These choices led to a grid that is 13 columns wide, and has alternately four or five hexels in the column, which requires 58 hexels, two fewer than the number of LEDs in the 60-NeoPixel strip. One could also choose to make a true four digit display, which would require 67 hexels and would let you display 24-hour time.
To address the display, I chose "axial coordinates" -- see the coordinates link, scroll down to axial and select "flat-topped". The two coordinates are q for the columns and r for the other coordinate, which increases to the right, but moving downward also -- really, go to that website; it is so cool. In this coordinate system, the memory storage is a bit inefficient, requiring 13 values of q and 10 values of r, but using 13 bits encoded in two 8-bit characters meant that 20 characters can hold the full bitmap, which is pretty small. The origin of the coordinate system is the center of possible symmetric forms, which makes it easier to check for symmetry (more on this later).
Step 3: Figure Out the Dimensions and Design the Circuit
NeoPixel strips are very cool for making linear structures, but of course a hexagonal array has many lines, so I needed to choose a direction. I wanted my display to be as big as possible to show the hexagonal nature, so I chose the wide spacing (30 NeoPixels / meter) and chose to run them vertically, in either four or five LED segments that were wired in a serpentine fashion. On this grid, the vertical spacing of LEDs is square root of 3 times the hexagon edge dimension. The strip having 30 NeoPixels / meter means a pixel spacing of 1.31 inches, so the hexagon's edge is 1.31 inches / sqrt(3) = 0.758 inches The separation between the columns of the array is then 3/2 times the hexagon edge size = 1.14 inches. It got pretty involved ... so I decided to write code to print out the hexagonal grid. The code and pre-made templates are available on GitHub. The template grid is produced at 600 DPI, which can be printed at that resolution (be careful that you printer isn't re-scaling!) to make the correct shape for the array. My picture frame had a glass dimension of 17 inches x 10 inches, so the full-grid template is scaled to that size. Because not many of us have large-format printers, I also include a half-grid template that can be printed on a normal US 8.5"x11" page of paper. Both versions are in the "GridTemplates" folder on GitHub.
The figure shows the wiring diagram for a 6-hexel display as an example. The microcontroller is powered over USB, which proved to be enough to also power the NeoPixels. The two other parts are a light sensor (more on that later) and a battery-backup real-time clock that keeps the time, which are connected via the I2C bus. A full-scale version of the wiring diagram is also on GitHub. It would be easy to adapt to any other microcontroller, but be forewarned that NeoPixels want 5V power and 5V digital logic signals to operate reliably and modern microcontrollers often use 3.3V logic, which might not work reliably.
Step 4: Build It
The picture above shows my physical build. This version had a separate power supply for the NeoPixels, but it proved unnecessary, so was later eliminated. I measured the locations of the pixels onto the back of the picture frame and then hot glued them down to the backing board. I then wired all of the pixels and glued down the other parts. Note that the +5V power for the NeoPixels is fed in on one bus and the ground on the other bus, while the digital control signal snakes down and up. Once you wire it, use a multimeter to assure that there is not a direct short between the +5V and ground rails!
Step 5: When at First You Don't Succeed
Becky Stern suggested a number of ways to diffuse LEDs, and a sheet of white paper works pretty well, but the light from one LED also illuminated the neighboring hexagonal area, leading to a non-crisp display. I tried many solutions for diffusing the LEDs and outlining the hexagonal array, but most were pretty poor. I realized I needed a hexagonal shadow grid that would isolate the hexels from their neighbors. I built it by cutting out "V"-shaped bits of black paper that would form two edges of a hexagon when bent at 120 degrees. The other dimension of this "V" is the spacing between the glass and the back plane of the picture frame, 1/2" in my case. Thus, the strips are 1+1/2" x 1/2" and bent in the middle. I used E6000 glue to attach the black "V"s to a transparency film. It took two 8.5"x11" transparency films to make the full grid. I printed out my grid on a page of paper and then overlaid the printed grid with the transparency film, spread a line of glue along the grid lines and then placed the "V"s on edge into this glue. I found that if you put down lines of glue and waited a few minutes for it to get tacky, it was easier to get the "V"s to stick where you wanted. Once the transparency films have the "V"s glued to them, I removed the paper grid templates and added a simple white sheet of paper as a diffuser. If you had a 3-D printer, you might get better results with less hassle. Overall, it probably took a little over an hour to build (not counting other failures...).
Step 6: The Full Grid
The photo above is the fully assembled shadow grid. As can be seen, light can leak through the gaps in the grid, which was a bit of an annoyance, so I glued on extra "V"s of paper over the worst of them. In the end, I think some of the light leakage give a more organic look to the clock, so I left some of them. Maybe I'll later fix them.
Step 7: The Final Product
Even with the shadow grid, I just couldn't get the hexels to look good until I saw in some videos in which people were using smoked plexiglas instead of clear glass on the front. I think it works because light coming from outside the box gets absorbed on the way in and out, so little re-emerges and it shows up as black, but the LEDs are quite bright and only pass the smokey film once, so they can be seen well. I was not able to get smoked plexiglas, but could find "shade control" window film at the hardware store, which worked really quite well. The photo in the header shows a blow-up of some of the hexels. You can see the E6000 glue acting as a bit of a lens, which was not desired, but seems OK and again lends an home-made flavor. I think that if you spray painted diffusing paint on the inside of the shadow grid grille, it might remove this effect but you don't really notice at a reasonable distance.
To review the hardware setup, from back to front: The back plane of the shadow-box frame is used to hold the LED array and electronics. The hexagonal shadow grid grille lies in front of the LEDs to allow the LED light to spread across the hexel. In front of the grille's transparency film, there is a sheet of white paper to diffuse the light, then the glass from the picture frame and last, on the outside, the "smoke" colored shade control film. I think the film could also be used on the inside of the glass, but I had a few bubbles which are less visible with the film on the outside, so I went with that.
Step 8: Write Code for the Clock
The code for the clock is available at the GitHub link. Please feel free to just use it or adapt it. The microcontroller I chose is the equivalent of an Arduino Uno, so I told the Arduino app that the "board" was an Arduino Uno, and I then selected its port and uploaded the code to it. Please see online help documents about how to upload the code and/or get the libraries (RTClib, FastLED, and possibly Adafruit_VEML7700 for the light sensor).
For those who are interested, some ideas from the code are below.
The basic data to be displayed is held in the "hexgrid", which is a bitmap where 13 values of the column index, q, are held in a 16-bit "word", and the 10 values of the "skewed row" index, r, are the index of the hexgrid array of words. The current hexgrid is displayed, but a prior hexgrid (last) is also kept for various calculations.
The NeoPixels are controlled by the excellent "FastLED" LED animation library, which requires a 58-element array of RGB colors to be displayed. To convert between the hexgrid and the pixel color values, we "walk" the hexgrid along the path that the NeoPixel data wire took in hardware. This path starts at the top left, goes down the first row (q is held constant, r is incremented) for four hexels, then r is held constant and q is incremented, which puts you in the lowest hexel of the next column. The index r is then decremented for five hexels, and then the process repeats. The "walk" algorithm is used for other purposes besides coloring, thus its uses are:
- Walking to "color": In this walk, the hexgrid bitmap is colored using various methods, but for any location on the grid, there will always only be an "on" or "off" color that is controlled by the bitmap's data.
- Walking for "Conway": Remember that the hexgrid is 13x10 = 130 possible values of q and r, but only 58 of these are on the display, so we can save some computational time by only calculating transitions for the displayable hexels.
- Walking for "symmetry": Here we check if the displayed hexgrid possesses inversion, vertical mirror (left/right), or horizontal mirror (top/bottom) symmetry, and if it does, the code always will choose the Conway transition. To check for symmetry, we iteratively examine symmetric points on the hexgrid to see if they match.
The color modes are:
- Mono: One foreground and one background in all locations
- Two-tone: Two different foregrounds, depending on the column of the display. This is used to distinguish between the hours and minutes
- Huewave: This mode has the color follow a hue wave that oscillates around the current foreground hue.
Between the minutes, a randomly selected transition is carried out for the last few seconds of the prior minute. As indicated earlier, if the pattern is symmetric, the Conway transition is always chosen. The current transition modes are:
- Conway: Plays Conway's game of life on a hexagonal grid with the rules that any cell with two neighbors alive will either remain alive or be born. Cells with fewer or more than two neighbors die.
- Swipeoff: Translates the hexgrid in one of the six directions of the hexagonal grid. The direction is randomly selected.
Please feel free to modify the code and make more interesting transitions and/or color schemes.
The real-time clock also has a temperature readout, so I also have the display show the temperature, which is accessed by blocking the light sensor (wave your hand in front of it) in the top middle of the picture frame. The temperature is then shown in °C and °F. It is funny to me that C and F were in our hexadecimal font set, and even look OK (unlike b and d).
Step 9: Serial Interface -- the Backdoor
Although the clock works fine on its own, I wanted to give it more capability, so I used a serial interface on the USB to transfer data also. First off, this connection is helpful for setting the correct time, but it is also used to change the colors or behavior. The commands are quite basic one-letter codes that are generally preceded by a numeric argument (represented by # in these comments). You can use the Arduino "Serial Monitor" to test out this interface (make sure to use 9600 baud). The most useful command sets the real-time clock:
- ####S = Set time to HHMM
For example, type "1255S" to set the time to 12:55. You can also decide on a color scheme you want to use. The standard color scheme is to display the minutes in the selected hue at full saturation and to display the hours in the same hue but only at 1/3 saturation (e.g., more white light than colored). The background is the opposite hue at low intensity. The relevant commands are:
- D = Set default colors (no argument)
- ###H = Set hue for display pattern (0...255) using FastLED spectrum HSV colorspace
- ###I = Set "inverse" hue for display pattern (0...255) -- typically horribly ugly and barely legible.
Any choice of the default hue you make will be retained until the USB power is cycled or the Arduino is reset, at which point the display will return to the default hue. The default hue is hard coded in the line #define DEFAULT_HUE, so you can change the value on this line and then upload the code to make the change permanent.
Other commands check information or play Conway at will:
V or ? = Show version, time, and temperature
Y = check the currently displayed hexgrid for symmetry (prints out to serial port)
####C = Enter Conway Mode for argument milliseconds
#W = Enter "swipeoff" mode in the argument direction, which has values from 0 (up), 1 (right and up) ... to 5 (left and up).
If you plug the clock's USB cable into a computer (I use a Raspberry Pi), the OS will assign it a port (e.g., /dev/ttyUSB0). I then use a terminal emulator (e.g., minicom, screen, or hyperterminal on windows and terminal on MacOS) to communicate with the clock directly using this port. The settings are 9600 baud, 8 data bits, no parity, 1 stop bit (9600 8N1). In this mode, you don't need to type enter, just type numbers for the argument and then a single letter for the command and it will happen. If you mess up, just type a random letter and start again. The display always reverts to the time, so you won't mess it up for too long. The serial port also gives you some feedback, which can be useful for debugging.
Step 10: Play With the Clock -- Have Fun
With the serial interface, you can play with the colors, which results in a few decent combinations and many terrible results as evident above, but you might find one you like.
The most interesting command is X, which allows you to transfer bitmaps directly on to the display. It is for advanced users because it generally requires that you access the serial port with some coding language like Python. The "X" command has a different syntax. It takes a pre-argument, which is how long to hold this pattern (in milliseconds), but then after typing the X, you need to pass twenty characters that contain the hexgrid to be displayed. These 20 characters are read without any filtering, so the normal commands won't work for the next 20 characters or the timeout happens. The timeout is set by #define XFER_TIMEOUT and is 500 milliseconds by default. If the 20 characters are not received, the buffer is cleared and normal command mode resumes. The command syntax is:
- ####X = Transfer binary hexgrid and hold for argument milliseconds
The 20-character array is made up of ten rows (r index) of 16 bit words (two 8-bit characters) encoding the q index. These characters are passed in MSB, LSB order, followed by the next row. To test, a novice can paste in a full X-command string (e.g. 5000XZZZZZZZZZZZZZZZZZZZZ) to the Arduino serial monitor or a terminal emulator, but the patterns made by type-able characters is typically pretty random. Since more interesting patterns require characters that are unprintable and the whole pattern needs to be sent faster than you can type, you really need to write code to push over bitmaps. I include some "helper" python code (fireworks.py or showppm.py) that uses the X command to show fireworks or the phrase "PPM" as examples. I calculate that at 9600 baud, the transferred hexgrids could go as fast as 60 Hz, so it could do some fast changes. I found that making the serial port's baud rate faster sometimes led to failures in transferring the data, which I think might be a conflict with FastLED, but that 9600 baud seemed reliable. If anybody gets interested in this project, it is my hope that the direct transfer could lead to interesting applications such as displaying CO2 mixing ratios (in PPM) from some another sensor or an online data source. One could also make a direct transfer mode that would transfer the color RGB values for the 58 hexels and use the device as a very tiny color monitor or do something more interesting.
I hope you enjoyed this project.
This is an entry in the
Make it Glow Contest