IoT Santa Tracker on Colorful World Map

Use MKR1000 to show the Santa location in real-time on a colorful world map made of neopixels.

Things used in this project

Hardware components

Arduino MKR1000
Arduino MKR1000
× 1
Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
× 1
FR-1 Printed Circuit Board Blanks
× 2
Translucent Colored Cast Acrylic (Sign Lighting White 40%)
× 1
Neopixel WS2812B
× 30
Micro USB female connector
× 1
SMD 10uF Capacitor
× 30
SMD 1000uF Capacitor
× 1
M3 Nylon screws, standoffs and nuts
× 4

Software apps and online services

Arduino IDE
Arduino IDE
Python
Flask
Autodesk Eagle CAD
Otherplan

Hand tools and fabrication machines

Bantam Tools Desktop PCB Milling Machine
Bantam Tools Desktop PCB Milling Machine
Soldering iron (generic)
Soldering iron (generic)
Hot Air Soldering Rework Station w/ Three Nozzles – Quick 957D

Story

Hohoho… Santa is here!

Every year the Christmas Eve Santa Claus sets out on a journey travels around the globe and sends out presents to all the children around the world. But would it be cool to know where the Santa Claus is at that day?

 

MKR1000 Santa Tracker to the rescue!

You may already know there are two places where we can get Santa location information, one is from NORAD and one from Google. Although NORAD was the original organization who started the Santa tracking tradition, but Google provides a developer friendly (undocumented) API for the Santa tracking. With this API you will be able to track the real time Santa information including the location, arrival and departure time, presents sent at the location, the same as on the Google’s Santa tracking web site. So in this project I chose to use Google Santa data to implement my Santa tracker.

The idea of this project is simple: Use LEDs to show where the Santa have been and the current location of Santa on a world map. Here is what I get at the end of the project:

System Architecture

Overall the design take use of one raspberry PI and one MKR1000 to process and visualize the Santa data fetched from Google Santa Tracker API.

As you can see the data fetched from Google Santa

Tracker API is first put though a Raspberry PI. The reason is that the API response JSON is around 20M which is too big to fit into MKR1000’s memory (18M available after my sketch is loaded) for processing. So I’m using a Raspberry Pi 3 to first consume the data, and generate a much smaller data format which is tailer made for my application. The later data is then exposed through a REST API server hosted on Raspberry Pi. The MKR1000 board will call the REST API every 10 seconds to get the current Santa location.

Circuit Design

The MKR1000 is connected to a custom PCB which has 30 WS2812B RGB Neopixel LEDs. Each LED represents one geo location. The idea is blinking the closest LED to where the Santa’s current location, and turn on all the LEDs on Santa’s past locations.

The PCB is optimized for being produced by Othermill as a double sided PCB, but should be also easily produced through online PCB services like OSH Park.

I spread 30 neopixels on the PCB, forming a world map. You may not be able to see it right now, but after coating with the negative mask and diffuser it would be easier to recognize.

Custom made PCB for MKR1000 and 30 Neopixels

The main consideration here to make this home-milling-friendly is the location of vias. Since the vias are drilled on PCB plate both sides are not connected. So you will need to solder them together on both side through a wire. And because of that, the vias can not be put under a SMT component like they usually do in commercial PCBs.

 

There are some design considerations behind the PCB file:

  • The power is delivered through a thick trunk wire and sinked into another thick ground wire. This is because 30 neopixels will draw quite a lot of current.
  • The neopixeles are connected as close as possible to the trunk power wire. This is to reduce voltage drop across multiple neopixels. Different voltage will result in slight difference in brightness and color.
  • A 1000 uF capacitor is connected close to the power source. This is recommended best practice to work with neopixels.
  • The vias are put outside of other SMT components because they will be connected by soldering both side manually, the solder joint will not fit under another SMT components. This wouldn’t be a problem on PCBs produced by commercial services, but should take into consideration when make PCM in home.
  • Make sure you leave enough space between wires. It will be easily shorted after soldering because the home made PCB don’t have the insulation coat. This can be done in Otherplan application by setting the trace clearance to a larger value than the default 0.006in (e.g. 0.06 in should be good enough). My first fully soldered board was found not working because of short circuit, and it’s to hard to fix than just make another one.
  • If you are going to work with plenty of SMT components, a hot air rework station will save you tons of time on soldering. Although it’s doable, it really doesn’t worth the effort to hand-solder the components one by one. And hot air will also make the components align to the the exact position automatically.

World Map Mask and Diffuser

Because the Othermill can mill directly from a SVG, I just use the world map found from wikimedia.org.

I also created another eagle file containing the drill holes. Those holes are used for mounting the mask to the circuit board.

Since I want to get highest possible milling precision, the best way to do that is using the alignment bracket. But the outline will definitely overlap with the bracket. The solution I found was first only cut the map without cutting the outline. Then remove the bracket without telling the software. Then start the milling just to drill holes and cut the outline. The software will use the same tool path to cut the outline at exactly position you want.

First cut the map with bracket on.

After the cutting finished, remove the bracket without letting Otherplan software know.

Raspberry Pi Preprocessing Server

The response from Google Santa Tracker API is a huge JSON file. This may be a problem for MKR1000, but not a problem at all for Raspberry Pi 3. So I set up a HTTP server to preprocess the JSON file and produce a smaller data for the MKR1000.

The Raspberry Pi server also maps the location directly to the index of the corresponding LED to further reduce the calculation on the MKR1000. To do this, I first manually assigned a coordinate to each of the LED, and then calculate the distance between each of the location in Santa’s path to the LED coordinates, find the closest LED to represent that location.

The server is written in Python, and use the Flask web framework to expose the REST endpoints to the MKR1000.

from flask import Flask
import requests
import json
import math
import sys
app = Flask(__name__)
# Google's Santa API. Only updates on Dec 24.
# santa_api_url = 'https://santa-api.appspot.com/info?client=web&language=en&fingerprint=&routeOffset=0&streamOffset=0'
# My Fake Santa API.
santa_api_url = 'http://localhost:1224/info'
# LEDs metadata.
leds = [
   {'name': 'North Pole', 'location': {'lat': 90.0, 'lng': 30.0}},
   {'name': 'Alaska (US)', 'location': {'lat': 64.536117, 'lng': -151.258768}},
   {'name': 'Alberta (Canada)', 'location': {'lat': 48.9202307, 'lng': -93.69738}},
   {'name': 'Ontario (Canada)', 'location': {'lat': 50.956252, 'lng': -87.369255}},
   {'name': 'Utah (US)', 'location': {'lat': 40.7765868, 'lng': -111.9905244}},
   {'name': 'Tennessee (US)', 'location': {'lat': 36.1865589, 'lng': -86.9253274}},
   {'name': 'Mexico City (Mexico)', 'location': {'lat': 19.39068, 'lng': -99.2836957}},
   {'name': 'Bogota (Columbia)', 'location': {'lat': 4.6482837, 'lng': -74.2478905}},
   {'name': 'Brasilia (Brazil)', 'location': {'lat': -15.721751, 'lng': -48.0082759}},
   {'name': 'Santiago (Chile)', 'location': {'lat': -33.4727092, 'lng': -70.7699135}},
   {'name': 'Greenland', 'location': {'lat': 70.8836652, 'lng': -59.6665893}},
   {'name': 'UK', 'location': {'lat': 64.6748061, 'lng': -7.9869018}},
   {'name': 'Spain', 'location': {'lat': 40.4379332, 'lng': -3.749576}},
   {'name': 'Mali', 'location': {'lat': 17.5237416, 'lng': -8.4791157}},
   {'name': 'Finland', 'location': {'lat': 64.6479136, 'lng': 17.1440256}},
   {'name': 'Greece', 'location': {'lat': 38.2540419, 'lng': 21.56707}},
   {'name': 'Libya', 'location': {'lat': 21.520733, 'lng': 23.237173}},
   {'name': 'Central African Republic', 'location': {'lat': 6.2540984, 'lng': -0.2809593}},
   {'name': 'Botswana', 'location': {'lat': -22.327399, 'lng': 22.4437318}},
   {'name': 'Saudi Arabia', 'location': {'lat': 24.0593214, 'lng': 40.6158589}},
   {'name': 'Turkmenistan', 'location': {'lat': 38.9423384, 'lng': 57.3349508}},
   {'name': 'Xinjiang (China)', 'location': {'lat': 42.0304225, 'lng': 77.3185349}},
   {'name': 'India', 'location': {'lat': 20.8925986, 'lng': 73.7613366}},
   {'name': 'Henan (China)', 'location': {'lat': 33.8541479, 'lng': 111.2634555}},
   {'name': 'Cambodia', 'location': {'lat': 12.2978202, 'lng': 103.8594626}},
   {'name': 'Japan', 'location': {'lat': 34.452585, 'lng': 125.382845}},
   {'name': 'Australia', 'location': {'lat': -25.0340388, 'lng': 115.2378468}},
   {'name': 'New Zealand', 'location': {'lat': -43.0225411, 'lng': 163.4767905}},
   {'name': 'South Pole', 'location': {'lat': -90.0, 'lng': 30.0}},
]
@app.route('/santa')
def santa():
   santa_info = requests.get(santa_api_url).json()
   santa_time = santa_info['now']
   response = []
   for dest_json in santa_info['destinations']:
       if santa_time < dest_json['arrival']:
           break
       dist, led, led_index = closest_led(dest_json['location'])
       response.append({
           'i': led_index,
           'd': int(dist),
           'n': dest_json['city'],
           'p': dest_json['presentsDelivered']
       })
   return app.response_class(json.dumps(response).replace(' ',''), content_type='application/json')
def distance(loc1, loc2, unit='M'):
   lat1 = loc1['lat']
   lng1 = loc1['lng']
   lat2 = loc2['lat']
   lng2 = loc2['lng']
   radlat1 = math.pi * lat1 / 180
   radlat2 = math.pi * lat2 / 180
   theta = lng1-lng2
   radtheta = math.pi * theta / 180
   dist = (math.sin(radlat1) * math.sin(radlat2) +
       math.cos(radlat1) * math.cos(radlat2) * math.cos(radtheta));
   dist = math.acos(dist)
   dist = dist * 180 / math.pi
   dist = dist * 60 * 1.1515
   if unit == 'K':
       return dist * 1.609344
   if unit == 'N':
       return dist * 0.8684
   return dist
def closest_led(loc):
   min_dist = sys.float_info.max
   min_led = None
   min_index = 0
   for index, led in enumerate(leds):
       led_loc = led['location']
       dist = distance(loc, led_loc)
       if dist < min_dist:
           min_dist = dist
           min_led = led
           min_index = index
   return min_dist, min_led, min_index
if __name__ == '__main__':
   app.run(host='0.0.0.0', port=2412)

Since the Google Santa Tracker API only updates on one day (Dec 24) in a year, in order to test the whole system, I also wrote a fake Santa tracker API server simulates the real one. With this fake API server, I can also control the speed of travel and reset as needed. This server is also a Flask Python server, run on a different port on the Raspberry Pi 3.

from flask import Flask, request
import json
import time
app = Flask(__name__)
fake_start_time = 0  # initialized to first arrival time from json
real_start_time = 0  # set to start time
speed_factor = 100  # fake clock speed
all_destinations = None
current_info = {
   'status': 'OK',
   'language': 'en',
   'now': None,            # Will be set to fake time
   'timeOffset': 120000,
   'fingerprint': '3b8835bc354c6d5018344b289b833402f7079844',
   'refresh': 51449,
   'switchOff': False,
   'clientSpecific': {
       'DisableEarth': False,
       'DisableTracker': False,
       'DisableWikipedia': False,
       'DisablePhotos': False,
       'HighResolutionPhotos': False,
       'EarthAltitudeMultiplier': 1
   },
   'routeOffset': 0,
   'destinations': None    # Will only have destinations up to two towns ahead
}
@app.route('/info')
def info():
   if real_start_time != 0:
       advance_fake_time()
   return app.response_class(json.dumps(current_info), content_type='application/json')
@app.route('/start')
def start():
   global real_start_time, speed_factor
   real_start_time = real_now()
   speed_factor = int(request.args.get('speed', '100'))
   print(u'fake clock stated at speed {0}'.format(speed_factor))
   return 'ok'
@app.route('/reset')
def reset():
   global real_start_time
   real_start_time = 0
   current_info['destinations'] = all_destinations[:3]
   return 'ok'
def index_of_current_destination(ts):
   for i, dest in enumerate(all_destinations):
       if dest['departure'] > ts:
           return i
   return 0
def current_destinations():
   index = index_of_current_destination(fake_now()) + 3
   return all_destinations[:index]
def advance_fake_time():
   current_info['now'] = fake_now()
   current_info['destinations'] = current_destinations()
def real_now():
   return int(time.time() * 100)
def fake_now():
   return (real_now() - real_start_time) * speed_factor + fake_start_time
def arrival(d):
   return d['arrival']
def load_json():
   with open('santa2016.json') as data_file:
       data = json.load(data_file)
   global all_destinations, fake_start_time
   all_destinations = sorted(data['destinations'], key=arrival)
   fake_start_time = all_destinations[1]['arrival']
   reset()
   print(u'{0} destinations loaded, fake_start_time={1}'.format(len(all_destinations), fake_start_time))
if __name__ == '__main__':
   load_json()
   app.run(host='0.0.0.0', port=1224)

MKR1000 Firmware

Now the MKR1000 is ready to fetch the data from Raspberry Pi server, and turn LEDs on and off.

#include <SPI.h>
#include <WiFi101.h>
#include <Adafruit_NeoPixel.h>
#include "JsonStreamingParser.h"
#include "JsonListener.h"
#define LED_PIN 6
#define LED_NUM 30
#define BRIGHTNESS 50
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800);
char ssid[] = "YOUR_SSID";     //  your network SSID (name)
char pass[] = "YOUR_PWRD";  // your network password
int keyIndex = 0;            // your network key Index number (needed only for WEP)
int status = WL_IDLE_STATUS;
IPAddress server(192, 168, 1, 120);  // numeric IP for RPI server
//char server[] = "rpi3.local";    // name address for RPI server
char endpoint[] = "/santa";
int port = 2412;
// Initialize the Ethernet client library
// with the IP address and port of the server
// that you want to connect to (port 80 is default for HTTP):
WiFiClient client;
class Led {
 public:
   String name;
   int distance;
   int presents;
   boolean on;
};
Led leds[30];
class LedSwitcher: public JsonListener {
 public:
   void whitespace(char c) {}
   void startDocument() {}
   void key(String key) {
     Serial.println(key);
     currentKey = key;
   }
   void value(String value) {
     Serial.println(value);
     if (currentKey == "i") {
       ledIndex = value.toInt();
     } else if (currentKey == "p") {
       presents = value.toInt();
     } else if (currentKey == "d") {
       distance = value.toInt();
     } else {
       name = value;
     }
   }
   void endArray() {}
   void endObject() {
     Serial.println("End of Object");
     Serial.print(ledIndex);
     Serial.print(":");
     Serial.print(name.c_str());
     Serial.print(",");
     Serial.print(presents);
     Serial.print(",");
     Serial.print(distance);
     leds[ledIndex].on = true;
     leds[ledIndex].name = name;
     leds[ledIndex].presents = presents;
     leds[ledIndex].distance = distance;
   }
   void endDocument() {}
   void startArray() {}
   void startObject() {}
   int lastLed() {
     return ledIndex;
   }
 private:
   String currentKey;
   int ledIndex;
   int presents;
   int distance;
   String name;
};
LedSwitcher ledSwitcher;
void connectToWifi() {
 // check for the presence of the shield:
 if (WiFi.status() == WL_NO_SHIELD) {
   Serial.println("WiFi shield not present");
   // don't continue:
   while (true);
 }
 // attempt to connect to Wifi network:
 while (status != WL_CONNECTED) {
   Serial.print("Attempting to connect to SSID: ");
   Serial.println(ssid);
   // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
   status = WiFi.begin(ssid, pass);
   // wait 10 seconds for connection:
   delay(10000);
 }
 Serial.println("Connected to wifi");
 printWifiStatus();
}
boolean connectToSantaServer() {
 Serial.println("Starting connection to server...");
 return client.connect(server, port);
}
void ensureConnected() {
 if (!client.connected()) {
   while (!connectToSantaServer()) {
     Serial.println("Failed to connect to server. Retry in 5 seconds");
     delay(5000);
   }
   Serial.println("connected to server");
 }
}
void fetchSantaInfo() {
 ensureConnected();
 // Make a HTTP request:
 client.print("GET ");
 client.print(endpoint);
 client.println(" HTTP/1.1");
 client.print("Host: ");
 client.println(server);
 client.println("Connection: close");
 client.println();
 int bytes = 0;
 boolean isBody = false;
 JsonStreamingParser parser;
 parser.setListener(&ledSwitcher);
 Serial.println();
 Serial.println("Received response:");
 Serial.println();
 while (client.connected()) {
   while (client.available()) {
     char c = client.read();
     ++bytes;
     //Serial.write(c);
     if (isBody || c == '[') {
       isBody = true;
       parser.parse(c);
     }
   }
 }
 Serial.println();
 Serial.println();
 Serial.println("Disconnecting from server.");
 client.stop();
 Serial.print("Received: ");
 Serial.print(bytes);
 Serial.println(" Bytes.");
}
void flashLastLed() {
 strip.setPixelColor(ledSwitcher.lastLed(), strip.Color(0, 0, 0));
 strip.show();
 delay(500);
 strip.setPixelColor(ledSwitcher.lastLed(), strip.Color(255, 0, 0));
 strip.show();
 delay(500);
}
void setup() {
 //Initialize serial and wait for port to open:
 Serial.begin(9600);
 strip.setBrightness(BRIGHTNESS);
 strip.begin();
 strip.show(); // Initialize all pixels to 'off'.
 connectToWifi();
 fetchSantaInfo();
 for (int i = 0; i < 30; ++i) {
   if (leds[i].on) {
     strip.setPixelColor(i, strip.Color(255, 0, 0));
   } else {
     strip.setPixelColor(i, strip.Color(0, 0, 0));
   }
 }
 strip.show();
}
void loop() {
 fetchSantaInfo();
 delay(1000);
 for (int i = 0; i < 10; ++i) {
   flashLastLed();
 }
}
void printWifiStatus() {
 // print the SSID of the network you're attached to:
 Serial.print("SSID: ");
 Serial.println(WiFi.SSID());
 // print your WiFi shield's IP address:
 IPAddress ip = WiFi.localIP();
 Serial.print("IP Address: ");
 Serial.println(ip);
 // print the received signal strength:
 long rssi = WiFi.RSSI();
 Serial.print("signal strength (RSSI):");
 Serial.print(rssi);
 Serial.println(" dBm");
}

As most of the work has been done on Raspberry Pi, the code here is straightforward. It connects to WiFi, the every 10 seconds it fetches the Santa data from Raspberry Pi, and update the neopixels accordingly.

Now let’s power it on:

 

Custom parts and enclosures

Schematics

Code

Everything for this project

All the source code for this project, including the MKR1000 firmware, software running on Raspberry Pi, and the hardware CAD files.

Source : IoT Santa Tracker on Colorful World Map


About The Author

Ibrar Ayyub

I am an experienced technical writer holding a Master's degree in computer science from BZU Multan, Pakistan University. With a background spanning various industries, particularly in home automation and engineering, I have honed my skills in crafting clear and concise content. Proficient in leveraging infographics and diagrams, I strive to simplify complex concepts for readers. My strength lies in thorough research and presenting information in a structured and logical format.

Follow Us:
LinkedinTwitter

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top