Introduction: Smart Map of Idaho With LED Data + Art

I've always wanted a way to artistically and dynamically display geographic data by "painting" a map with light. I live in Idaho and love my state so I thought this would be a great place to start! In addition to being a piece of art with cool illumination effects, it provides useful information as well. For example, you can show a "heat map" per count of population density, precipitation levels, elevation maximums/minimums, number of acres of wilderness area, etc. After doing this map of Idaho, I'm motivated to do something similar on a global scale!

For this instructable you will need the following:

  • (2) 2'x4' sheet of 1/4MDF
  • (1) 10' piece 1"x8" Pine board
  • (1) sheet of light diffusing acrylic
  • 2 Strings of (50) ws2812B prewired indexible LED
  • 5 volt power supply
  • Stain, paint, glue
  • Arduino Micro or equivalent

Tools Needed

  • CNC Machine
  • Soldering iron
  • Clamps
  • Sand Paper

Step 1: Glue Up the Wood

Whenever I glue up panels of wood, I always biscuit join them together. This prevents splitting due to shrinkage as the wood dries. This is especially important on this project as the surface area connecting the pieces together will be reduced due to the CNC pocketing operations. After getting a good bead of glue in on both sides as well as in both halves of the biscuit cavities, clamp and leave for 24 hours.

After pulling the clamps apart, use a palm sander (or if you are brave a belt sander) and sand the joints smooth. Invariably you will have glue squeeze out the joints and you want to sand the board down to be as flat and blemish free as possible.

now that we have the three panels we will be needing let's move on to the CNC work!

Step 2: CNC the Three Panels (Border Panel, Pine Core and LED Panel)

There are three panels that make up the project. You can see the models in the software I use. The map data was purchased from the excellent royalty free maptorian map packs. Amazing detail and value here! The CAD files are attached in the next step if you'd like either the DXF for the CAD or the Vector files.

The LED core panel is basically a machined 1/4" MDF sheet that holds the LEDs with a tight friction fit. You will notice on this panel a large "pocket" around the LED. This is to allow the light to diffuse as quickly as possible so to avoid hot spots on the acrylic.

The core is the pine panel we glued up in the previous step and represents the backdrop for the project. In order for the light to reach the acrylic panels we machined away each county.

Finally the top panel is machined with just the outlines of the counties and the state boarder. Each county has a small shelf that will receive the 1/8" light diffusing acrylic.

Speaking of acrylic, time to machine these next.

Step 3: Machine the Counties From an Acrylic Sheet

Machining the counties from acrylic took a little trial and error. Acrylic can melt if machined to slowly so a proper feedrate is necessary to get good results. Another tip is to use as large a tool as possible with good suction to clear away the chips. Small tools tend to not clear the chips as easily and build up heat that produces that undesirable melting.

I was able to get the resolution I needed with an 1/8" upcut two flute spiral bit at 18,500rpm and a feedrate of 200ipm. A good feeds and speeds calculator is useful here! I'd recommend the one at cnccookbook.com. A single flute bit would have worked even better but I didn't have one on hand. Keeping small tabs on these pieces in the CAM work is important to keep the finished pieces from breaking away and getting projected into the room!

The magic offset for making counties the right size, turned out to be .075 setback from the centerline on the cad drawing. This made allowance for 1/2 of the 1/8" border plus a little extra for the panel to drop in place. A small amount of sanding was required on certain pieces to get them to fall into place. Again, a bunch of friction fit pieces made this quick and easy work.

Getting all the counties to fit onto a single piece of acrylic was easy work with my vectric software that has a nesting feature to maximize sheet usage.

Just for fun I started test fitting some pieces. Starting to come along. Cool!

Want the files for machining the counties. Sure! See attachment.

Step 4: Paint and Stain

Before we assemble all our pieces, we should paint and stain first. I used a combination of stains for the wood panel, spray paint for the border layer and a reflective white for the LED layer. Quick work and we are on to the assembly. Having fun!

Step 5: Glue Up Panels

Now it's time to glue up the bottom panel to the bottom of the pine core and then the MDF state border panel to the to of the pine core. I just used a series of clamps to do this.

Step 6: Wire Up the LED's With Friction Fit and Connect Arduino

This nightmare job was super simple with the friction fit tolerances here. I used the back end of a pen to press them into place . The virtually snapped in and will not come out without significant force. No glue of any sort was used for this part of the project. This makes assembly, WAY easy! I've done many projects where I've had to wrangle wiring for hours and this literally took 10 minutes. This is by far the easiest way. I tried to wire up the state in zigzag order keeping the groupings such that each county was sequential along the string.

Connecting to the arduino was simple through the use of a small breadboard and connecting wires. The power supply was an ebay purchase. 5v and 8amps is overkill for this project but gives plenty of overhead. Wiring these things is dead simple. +5v to the VCC pin, Ground to the ground pin and then power the strand with the same 5v source. The only remaining pin is the data pin that powers the string! In my case, I used D7 for data. Now on to programming!

Step 7: Coding the Arduino

The LED's are powered by an arduino which makes coding cake. Some of the initial routines were borrowed (ie. stolen) from the excellent ws2813fx library on github. It was easy to modify these routines to do what I needed them to do. The full scope of the code would be difficult to explain in entirety but here's a few highlights!

Here's the available demonstration routines:

<p>#define FX_MODE_STATIC                   0<br>#define FX_MODE_BLINK                    1
#define FX_MODE_BREATH                   2
#define FX_MODE_COLOR_WIPE               3
#define FX_MODE_COLOR_WIPE_INV           4 
#define FX_MODE_COLOR_WIPE_REV           5
#define FX_MODE_COLOR_WIPE_REV_INV       6
#define FX_MODE_COLOR_WIPE_RANDOM        7
#define FX_MODE_RANDOM_COLOR             8
#define FX_MODE_SINGLE_DYNAMIC           9
#define FX_MODE_MULTI_DYNAMIC           10
#define FX_MODE_RAINBOW                 11
#define FX_MODE_RAINBOW_CYCLE           12
#define FX_MODE_SCAN                    13
#define FX_MODE_DUAL_SCAN               14
#define FX_MODE_FADE                    15
#define FX_MODE_THEATER_CHASE           16
#define FX_MODE_THEATER_CHASE_RAINBOW   17
#define FX_MODE_RUNNING_LIGHTS          18
#define FX_MODE_TWINKLE                 19
#define FX_MODE_TWINKLE_RANDOM          20
#define FX_MODE_TWINKLE_FADE            21
#define FX_MODE_TWINKLE_FADE_RANDOM     22
#define FX_MODE_SPARKLE                 23
#define FX_MODE_FLASH_SPARKLE           24
#define FX_MODE_HYPER_SPARKLE           25
#define FX_MODE_STROBE                  26
#define FX_MODE_STROBE_RAINBOW          27
#define FX_MODE_MULTI_STROBE            28
#define FX_MODE_BLINK_RAINBOW           29
#define FX_MODE_CHASE_WHITE             30
#define FX_MODE_CHASE_COLOR             31
#define FX_MODE_CHASE_RANDOM            32
#define FX_MODE_CHASE_RAINBOW           33
#define FX_MODE_CHASE_FLASH             34
#define FX_MODE_CHASE_FLASH_RANDOM      35
#define FX_MODE_CHASE_RAINBOW_WHITE     36
#define FX_MODE_CHASE_BLACKOUT          37
#define FX_MODE_CHASE_BLACKOUT_RAINBOW  38
#define FX_MODE_COLOR_SWEEP_RANDOM      39
#define FX_MODE_RUNNING_COLOR           40
#define FX_MODE_RUNNING_RED_BLUE        41
#define FX_MODE_RUNNING_RANDOM          42
#define FX_MODE_LARSON_SCANNER          43
#define FX_MODE_COMET                   44
#define FX_MODE_FIREWORKS               45
#define FX_MODE_FIREWORKS_RANDOM        46
#define FX_MODE_MERRY_CHRISTMAS         47
#define FX_MODE_FIRE_FLICKER            48
#define FX_MODE_FIRE_FLICKER_SOFT       49
#define FX_MODE_FIRE_FLICKER_INTENSE    50
#define FX_MODE_CIRCUS_COMBUSTUS        51
#define FX_MODE_HALLOWEEN               52
#define FX_MODE_BICOLOR_CHASE           53
#define FX_MODE_TRICOLOR_CHASE          54
#define FX_MODE_ICU                     55</p>

And a look into one of the sample routines.

<p>uint16_t WS2812FX::mode_breath(void) {<br>  //                                      0    1    2   3   4   5   6    7   8   9  10  11   12   13   14   15   16    // step
  uint16_t breath_delay_steps[] =     {   7,   9,  13, 15, 16, 17, 18, 930, 19, 18, 15, 13,   9,   7,   4,   5,  10 }; // magic numbers for breathing LED
  uint8_t breath_brightness_steps[] = { 150, 125, 100, 75, 50, 25, 16,  15, 16, 25, 50, 75, 100, 125, 150, 220, 255 }; // even more magic numbers!</p><p>  if(SEGMENT_RUNTIME.counter_mode_call == 0) {
    SEGMENT_RUNTIME.aux_param = breath_brightness_steps[0] + 1; // we use aux_param to store the brightness
  }</p><p>  uint8_t breath_brightness = SEGMENT_RUNTIME.aux_param;</p><p>  if(SEGMENT_RUNTIME.counter_mode_step < 8) {
    breath_brightness--;
  } else {
    breath_brightness++;
  }</p><p>  // update index of current delay when target brightness is reached, start over after the last step
  if(breath_brightness == breath_brightness_steps[SEGMENT_RUNTIME.counter_mode_step]) {
    SEGMENT_RUNTIME.counter_mode_step = (SEGMENT_RUNTIME.counter_mode_step + 1) % (sizeof(breath_brightness_steps)/sizeof(uint8_t));
  }</p><p>  int lum = map(breath_brightness, 0, 255, 0, _brightness);  // keep luminosity below brightness set by user
  uint8_t w = (SEGMENT.colors[0] >> 24 & 0xFF) * lum / _brightness; // modify RGBW colors with brightness info
  uint8_t r = (SEGMENT.colors[0] >> 16 & 0xFF) * lum / _brightness;
  uint8_t g = (SEGMENT.colors[0] >>  8 & 0xFF) * lum / _brightness;
  uint8_t b = (SEGMENT.colors[0]       & 0xFF) * lum / _brightness;
  for(uint16_t i=SEGMENT.start; i <= SEGMENT.stop; i++) {
    Adafruit_NeoPixel::setPixelColor(i, r, g, b, w);
  }</p><p>  SEGMENT_RUNTIME.aux_param = breath_brightness;
  return breath_delay_steps[SEGMENT_RUNTIME.counter_mode_step];
}</p>

Full source can be downloaded from the ws2812fx github repository.

Step 8: Enjoy the Artistic Light Display

I was very happy with the result! It really is a joy to watch and I'm excited to continue to play around with various data display configurations! Feel free to ask any questions or hit me up for any information I missed.