Showing posts with label LED. Show all posts
Showing posts with label LED. Show all posts

Friday, December 20, 2024

WS2812B LED Ring Clock

 -----

There are no shortage of DIY clocks on the internet, but inspiration from this Hack a Day article made us think there is room for at least one more. One major different with ours is we didn't want to wait 30+ years to finish the project so we took the easy way out with a 93 x WS2812B LED setup in a 6 ring configuration. We had a HelTec WiFi 32 Dev Board 'in stock' with a bad display so that became the microcontroller of choice.

-----

The schematic is super simple.  In addition to just showing the boring time we added a "Disco Mode" button that creates a light show (video below).

-----

We needed a box also.  Here are the 3D Printer files: Box and Cover.  

Bolt it all together and this is what is does:  Connects to WiFi, gets current time of day, displays the time.  A quick press of the BRIGHTNESS/RESET button will cycle through 10 LED brightness levels and if you press and hold the rig will RESET.   The DISCO MODE button will put on a LED light show for 10 secs.  The light show also displays for 60 seconds at the top of the hour.

-----

Of course, the software is what makes it work.  The Arduino IDE software is below (yes, ChatGPT helped make things both easier and harder depending on the task):/*
 * "LED Ring Clock" shows time using 6 Ring WS2812B 93-LED Strip
 *           Outer ring=0-31, 5th=32-55, 4th=56-71, 3rd=72-83, 2nd=84-91, Center=92
 *
 * HelTec Automation(TM) ESP32 Series Dev boards OLED (OLED not used.  Selected this uC only because we had it.)
 * Adruino IDE Board Setting: WiFi Kit32, Disabled, 240MHz, 921600, None
 *
 * DEC 2024
 * Project details at: whiskeytangohotel.com
 *
*/

#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <time.h>

// Configuration for LED strip
#define LED_PIN 5         // Pin where the LED strip is connected
#define NUM_LEDS 93       // Total number of LEDs in the strip
#define BUTTON_PIN 13      // Pin connected to NO switch button to control Brightness and 'long press' RESET
#define DISCO_BUTTON_PIN 12  // Pin connected to button for Disco Mode

// Define up some variables
// Wi-Fi credentials
const char* ssid = "URSSID";           // Replace with your Wi-Fi SSID
const char* password = "URPASSWORD";   // Replace with your Wi-Fi password

int i = 0;
unsigned long ringTime = 1000; // later adjust ringTime to outer ring 1 sec round trip time
unsigned long endTime = 0;
unsigned long lastTimeUpdate = 999999999;   // set in printLocalTime() Function to control how often we check the time server
unsigned long discoStartTime = 0;   // Tracks when Disco Mode started

unsigned long buttonPressStart = 0; // Timestamp when the button was first pressed
bool buttonPressed = false;         // Tracks the current NO Switch button state on PIN 4
int brightnessLevel = 3;            // 0 - 255, but we cycle 1-10 cuz is bright. 100 is really really damn bright!

int currentHour = 0;
int currentMinute = 0;
int currentSecond = 0;
int LEDseconds = 32;

// Timezone settings for Austin, Texas.  Adjust for your timezone
const char* ntpServer = "pool.ntp.org";  // NTP server
const long gmtOffset_sec = -21600;      // Offset for CST (UTC -6)
const int daylightOffset_sec = 3600;   // Daylight Saving Time offset

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// Set up some colors to make it easier to customize the clock
uint32_t Red = 0xFF0000;
uint32_t Green = 0x00FF00;
uint32_t Blue = 0x0000FF;
uint32_t Yellow = 0xFFFF00;
uint32_t Cyan= 0x00FFFF;
uint32_t Magenta    = 0xFF00FF;
uint32_t White    = 0xFFFFFF;
uint32_t Black_Off =    0x000000;

// Now, Customize by choosing the color for the selected LED function
int Middle_blink = Red;      //color for the 2 inner rings
int Fast_seconds = Middle_blink;      // color for the fast paced outer ring
int Normal_seconds = Middle_blink;    // adjacent to the outer ring
int Minute_hand = White;         // 4th from center
int Hour_hand = Minute_hand;           // 3rd from center
int How_bright = brightnessLevel;      // Cycles from 1 to 10    

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Set button pin with internal pull-up resistor
  pinMode(DISCO_BUTTON_PIN, INPUT_PULLUP); // Button for Disco Mode
  strip.setBrightness(brightnessLevel); // Initialize brightness
 
  // Initialize LED strip
  strip.begin();
  strip.show();
  strip.clear();
  strip.setBrightness(How_bright);

  // Connect to Wi-Fi
  Serial.print("Connecting to Wi-Fi [flash RED]...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // Toggle LEDs 84 to 92 to show trying to connect
    for (int j = 84; j <= 92; j++) {
      uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
      if (currentColor == 0) { // If the LED is off
        strip.setPixelColor(j, strip.Color(255, 0, 0));                 
      }else { // If the LED is on
        strip.setPixelColor(j, 0); // Turn it off
      }        
    }  // End j loop for center LED toggle
    strip.show(); // Update the strip
    delay(500);
  }
  Serial.println("\nWi-Fi connected [flash GREEN]!!!");
  for (int i = 0; i <11; i++) {  // fast flash the inner LEDs to show 'wifi connected'
      for (int j = 84; j <= 92; j++) {
      uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
      if (currentColor == 0) { // If the LED is off
        strip.setPixelColor(j, strip.Color(0, 255, 0)); // Define RGB color
      }else { // If the LED is on
        strip.setPixelColor(j, 0); // Turn it off
      }  
    }  // End j loop for center LED toggle
    strip.show(); // Update the strip
    delay(200);
  }

  // Center LED Red to show we are trying to get the time
  strip.setPixelColor(92, strip.Color(255, 0, 0)); // Define RGB color
  strip.show(); // Update the strip
  delay(100);
  // Initialize time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();  // Print the current local time to the Serial Monitor
}

void loop() {  
  // Scanner effect for the first 32 LEDs on the outter ring
  unsigned long startTime = millis();  // We time the loop and aim for 1 sec lap time
 
  for (i = 0; i < 32; i++) {   
    handleButtonPress();   // is the Button pressed for Brightness or RESET?
    runDiscoMode();  // Disco Mode pressed?
    strip.setPixelColor(i, Fast_seconds); // Define RGB color
    strip.show();  // Update the strip    
    unsigned long elapsedTime = millis() - startTime; // Calculate elapsed time
    unsigned long delayPerLED = (1000 - elapsedTime) / (32 - i); // Dynamically adjust delay
    delay(delayPerLED); // Apply calculated delay
    strip.setPixelColor(i, strip.Color(0, 0, 0)); // Turn off the current LED

    if (i == 0) {   // Update the clock 'hands'
      // Toggle LEDs 84 to 92 (the center LEDs at 1Hz)
      for (int j = 84; j <= 92; j++) {
        uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
        if (currentColor == 0) { // If the LED is off     
          strip.setPixelColor(j, Middle_blink); // ON and Define RGB color  
        }else { // If the LED is on
          strip.setPixelColor(j, 0); // Turn it off
        }      
      }  // End j loop for center LED 1 Hz toggle
      
      uint32_t currentColor = strip.getPixelColor(91); // Flash center LED at opposite ON/OFF of surrounding ring   
      if (currentColor == 0) { // If the near LED is off   
        strip.clear();  // This helps avoid 'artifacts' just in case
        strip.setPixelColor(92, Middle_blink);  
      }

      // Position minute hand (4th ring with 16 LEDs for limited resolution)
      // 56(top) and 71 is last, so...  
      int minuteLED = 56 + (currentMinute / 3.75);
      strip.setPixelColor(minuteLED, Minute_hand);

      // Position hour hand (3rd ring with 12 LEDs.  LED72=12 Noon.  LED83=11]
      if (int(currentHour == 12)) {   // We want LED 72, not LED 12 + 72
        currentHour = 0;  
      }
      int LEDhour = int(currentHour) + 72;
      strip.setPixelColor(LEDhour, Hour_hand); // Define RGB color   
      // End position Hour Hand
      
      // Position the second hand (4th ring: LED 32 = TOP, LED 55 = LAST, 24 LEDs in total)
      strip.setPixelColor(LEDseconds, 0); // Turn off the previous second's LED      
      // Update LEDseconds and ensure it stays within the range [32, 55]
      LEDseconds = (LEDseconds < 55) ? LEDseconds + 1 : 32;  // less than 55 then add +1 else reset LEDseconds to 32
      strip.setPixelColor(LEDseconds, Normal_seconds); // Turn on the current second's LED
      // End Position Second Hand

    }  // End if i == 0 to Update the clock 'hands'
  }  // End For i loop for outer 'scanner' ring

  unsigned long endTime = millis();
  ringTime = endTime - startTime;  // How long did a full outer scanner ring take
  Serial.println();
  //Serial.println("Outer ring time: " + String(ringTime) + " ms");
  printLocalTime();  // Print Local time  
}

void printLocalTime() {
  // Update from the time server every 60 secs (1 min = 60000 milliseconds)
  if (millis() - lastTimeUpdate < 60000) {
    // Print the current local time from global variables
    Serial.print("Current local time: ");
    Serial.print(currentHour);
    Serial.print(":");
    if (currentMinute < 10) Serial.print("0"); // Add leading zero to minutes if needed
    Serial.print(currentMinute);
    Serial.print(":");
    if (currentSecond < 10) Serial.print("0"); // Add leading zero to seconds if needed
    Serial.println(currentSecond);
    Serial.println( "Next time update in: " + String(60 - (millis() - lastTimeUpdate)/1000) + " seconds." );
    Serial.println("Brightness set to: " + String(brightnessLevel));
    return;  // Exit the function without updating the time
  }

  // Fetch time from the NTP server
  struct tm timeinfo;
  Serial.println("Fetching time from the NTP server...");
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    strip.setPixelColor(92, Red); // Error code on center LED
    strip.show();
    delay(500);
    return;
  }

  int hour = timeinfo.tm_hour;
  String meridian = "AM";
 
  // Convert 24-hour to 12-hour format
  if (hour >= 12) {
    meridian = "PM";
    if (hour > 12) hour -= 12;  // Convert to 12-hour format
  } else if (hour == 0) {
    hour = 12; // Midnight in 12-hour format
  }

  // Update global variables
  currentHour = hour;
  currentMinute = timeinfo.tm_min;
  currentSecond = timeinfo.tm_sec;

  // Print time in 12-hour format
  Serial.print("Current local time: ");
  Serial.print(hour);
  Serial.print(":");
  if (timeinfo.tm_min < 10) Serial.print("0"); // Add leading zero to minutes if needed
  Serial.print(timeinfo.tm_min);
  Serial.print(":");
  if (timeinfo.tm_sec < 10) Serial.print("0"); // Add leading zero to seconds if needed
  Serial.print(timeinfo.tm_sec);
  Serial.print(" ");
  Serial.println(meridian);
  // Update the last time update to the current time
  lastTimeUpdate = millis();
}

void handleButtonPress() {   
  static bool showingBrightness = false;  // Tracks if brightness feedback is being displayed
  static unsigned long brightnessDisplayStart = 0; // Tracks when feedback started

  bool buttonState = digitalRead(BUTTON_PIN); // Read button state

  if (buttonState == LOW) { // Button pressed (LOW because of pull-up)
    if (!buttonPressed) {
      buttonPressed = true;
      buttonPressStart = millis(); // Record the press start time
    }

    // Check if the button is held for 5 seconds
    if (millis() - buttonPressStart >= 5000) {
      Serial.println("Resetting ESP32...");
      ESP.restart(); // Reset the ESP32
    }
  } else { // Button released
    if (buttonPressed) { // Only act if it was pressed before
      buttonPressed = false;

      if (millis() - buttonPressStart < 5000) { // Short press
        // Cycle brightness level (1 to 9)
        brightnessLevel = (brightnessLevel % 9) + 1;
        strip.setBrightness(brightnessLevel);
        strip.show(); // Apply the new brightness
        Serial.println("Brightness adjusted to: " + String(brightnessLevel));

        // Start brightness feedback display
        showingBrightness = true;
        brightnessDisplayStart = millis();
        strip.clear(); // Clear previous LED states
        for (int i = 84; i < 84 + brightnessLevel; i++) {
          strip.setPixelColor(i, Magenta); // Brightness level feedback via middle LEDs
        }
        strip.show();
      }
    }
  }

  // Handle non-blocking brightness feedback display
  if (showingBrightness && (millis() - brightnessDisplayStart >= 1000)) {
    // Turn off LEDs after 1 second
    showingBrightness = false;
    for (int i = 84; i < 92; i++) {
      strip.setPixelColor(i, 0); // Turn off the LEDs
    }
    strip.show();
  }
}

void runDiscoMode() { //Flash all the LEDs in random colors
  static unsigned long lastUpdate = 0;      // Tracks last LED update time
  static unsigned long discoStartTime = 0; // Tracks when Disco Mode started
  static bool discoActive = false;         // Tracks if Disco Mode is active
  static bool buttonStateLast = LOW;       // Tracks previous button state

  // Determine the button state of the black NC button
  bool buttonState;
  int TopofHour = 0;  // We Disco Mode flash for the whole minute
  if (currentMinute == TopofHour) {
    buttonState = HIGH; // Force Disco Mode at the top of the hour
  } else {
    buttonState = digitalRead(DISCO_BUTTON_PIN); // Read the actual button state
  }

  // Toggle Disco Mode on button press (HIGH) with debounce
  if (buttonState == HIGH && buttonStateLast == LOW) { // Button was just pressed
    discoActive = !discoActive; // Toggle discoActive
    if (discoActive) {
      Serial.println("Entering Disco Mode...");
      discoStartTime = millis(); // Record the start time
    } else {
      Serial.println("Exiting Disco Mode via button press...");
      strip.clear();
      strip.show(); // Clear LEDs
    }
  }

  if (currentMinute == TopofHour) {  // remain in Disco Mode
    buttonState = HIGH; // Force Disco Mode at the top of the hour
  } else {
    buttonStateLast = buttonState; // Update last button state
  }  

  // If Disco Mode is active
  if (discoActive) {    
    if (millis() - discoStartTime >= 10000) {   // Exit Disco Mode after set delay in milliseconds
      Serial.println("Exiting Disco Mode via timeout...");
      discoActive = false;
      strip.clear();
      strip.show(); // Clear LEDs
      return;
    }

    // Update LEDs non-blocking
    if (millis() - lastUpdate > 10) {  // in mSecs delay on the flashes.  i think faster is better
      lastUpdate = millis();
      for (int i = 0; i < NUM_LEDS; i++) {
        strip.setPixelColor(i, strip.Color(random(256), random(256), random(256)));
      }
      strip.show();
      //Serial.println("Update disco...");
    }
  }
}

-----

Monday, April 29, 2024

Where is the ISS? [Rasberry Pi version]

  

-----

It all started out simple enough... We were just curious how Orbital Files and Kepler Elements are use to define the current and predicted locations of a satellite.  One thing leads to another and the result was an International Space Station tracker.  Sure, it's been done before but you can learn a few things when you do it yourself.

-----

For example, we wrote the Python3 program on our own and then decided we would see how ChatGPT would handle the problem.   Turns out we were a lot faster at the task than the ChatGPT AI.  ChatGPT would finally get there, but it seemed error prone.  Still, impressively ChatGPT finally got to a solution after a TON of help from us.  But... we did like some of the things ChatGPT did better.  For example, the ChatGPT routine to translate compass degrees to a cardinal direction was better than ours.  So, in addition to learning about Orbital Files and Kelper Elements we learned a little about what ChatGPT is good and "less good" at.

-----

The project updates the location on the ISS for your location every ten seconds.  If the ISS is above the horizon a LED flashes to let you know.  The whole enchilada is contained in a 3D Printed box.

Here's the result and we like it!

-----

The connection of the 16x2 LCD and the indicator LED are simple and pretty obvious from the pin names in the source code below.

-----

For those that may want to duplicate the build below is our Python3 source code:

# ISS Tracker
# Project details at: WhiskeyTangoHotel.Com
#                     APRIL 2024
#
# Raspberry PI 3.  Runs under Python3
#
# Show current Alt and Az of the ISS
# Results are displayed in the terminal and 1602 Line LCD
# LED Blinks when ISS is above horizon.

from datetime import datetime, timedelta
from skyfield.api import Topos, load
import os
import urllib.request
import time
import pytz
import smbus
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
risefall = "Calculating..."   # Is the ISS getting closer or farther from the horizon
seconds_between_screens = 5  # How long to show the position and current time screen

# Set up the 16 x 2 Line I2C LCD
I2C_ADDR  = 0x27 # I2C device address
LCD_WIDTH = 16   # Maximum characters per line

LCD_CHR = 1 # Mode - Sending data
LCD_CMD = 0 # Mode - Sending command

LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line
LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line

LCD_BACKLIGHT  = 0x08  # On
# LCD_BACKLIGHT = 0x00  # Off

ENABLE = 0b00000100 # Enable bit

# Timing constants
E_PULSE = 0.0005
E_DELAY = 0.0005

# Open I2C interface
bus = smbus.SMBus(1) # Rev 2 Pi uses 1 RasPI(SXSW)

# LED pin to blink if ISS is above horizon
LED_PIN = 17  

def setup_led():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(LED_PIN, GPIO.OUT)

def led_on():
    GPIO.output(LED_PIN, GPIO.HIGH)

def led_off():
    GPIO.output(LED_PIN, GPIO.LOW)

def lcd_init():
    # Initialise display
    lcd_byte(0x33,LCD_CMD) # 110011 Initialise
    lcd_byte(0x32,LCD_CMD) # 110010 Initialise
    lcd_byte(0x06,LCD_CMD) # 000110 Cursor move direction
    lcd_byte(0x0C,LCD_CMD) # 001100 Display On,Cursor Off, Blink Off
    lcd_byte(0x28,LCD_CMD) # 101000 Data length, number of lines, font size
    lcd_byte(0x01,LCD_CMD) # 000001 Clear display
    time.sleep(E_DELAY)

def lcd_byte(bits, mode):
    # Send byte to data pins
    # bits = the data
    # mode = 1 for data
    #        0 for command

    bits_high = mode | (bits & 0xF0) | LCD_BACKLIGHT
    bits_low = mode | ((bits<<4) & 0xF0) | LCD_BACKLIGHT

    # High bits
    bus.write_byte(I2C_ADDR, bits_high)
    lcd_toggle_enable(bits_high)

    # Low bits
    bus.write_byte(I2C_ADDR, bits_low)
    lcd_toggle_enable(bits_low)

def lcd_toggle_enable(bits):
    # Toggle enable
    time.sleep(E_DELAY)
    bus.write_byte(I2C_ADDR, (bits | ENABLE))
    time.sleep(E_PULSE)
    bus.write_byte(I2C_ADDR,(bits & ~ENABLE))
    time.sleep(E_DELAY)

def lcd_string(message,line):
    # Send string to display
    message = message.ljust(LCD_WIDTH," ")
    lcd_byte(line, LCD_CMD)

    for i in range(LCD_WIDTH):
        lcd_byte(ord(message[i]),LCD_CHR)

# We set up to view the ISS with Austin, Texas coordinates
austin = Topos(latitude=30.35307, longitude=-97.85726)

# URL to fetch TLE data
tle_url = 'https://www.celestrak.com/NORAD/elements/stations.txt'

# Let's do self-test on the LCD and the LED
lcd_init()

# Center the text for line 1
alt_text = "ISS TRACKER"
print("ISS TRACKER")
spaces = (LCD_WIDTH - len(alt_text)) // 2
alt_text = " " * spaces + alt_text
lcd_string(alt_text, LCD_LINE_1)

# Flash LED self-test
setup_led()

for _ in range(5):  # Flash LED
    # Center the text for line 2
    az_text = "Starts in " + str(5 - _) + "..."
    print("Starts in " + str(5 - _) + "...")
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)
    led_on()
    time.sleep(0.5)  # LED on for 0.5 seconds
    led_off()
    time.sleep(0.5)  # LED off for 0.5 seconds
GPIO.cleanup()

print(" ")

last_altitude = None  # Variable to store the previous altitude

while True:  # Loop forever...
    # Initialise display
    lcd_init()

    # Check if the TLE data file exists
    if not os.path.exists('stations.txt'):
        # If it doesn't exist, download it
        urllib.request.urlretrieve(tle_url, 'stations.txt')

    # Get the last modified time of the TLE data file
    last_modified = datetime.fromtimestamp(os.path.getmtime('stations.txt'))

    # Check if 12 hours have passed since the last update
    if datetime.now() - last_modified > timedelta(hours=12):
        # If it is too old, update the TLE data file
        urllib.request.urlretrieve(tle_url, 'stations.txt')

    # Load the updated ISS TLE data
    satellites = load.tle_file('stations.txt')
    iss = satellites[0]  # Accessing the first element which contains the ISS data

    # Get the current time
    ts = load.timescale()
    current_time = ts.now()

    # Get the position of the ISS relative to Austin, Texas
    difference = iss - austin
    topocentric = difference.at(current_time)
    alt, az, distance = topocentric.altaz()
    
    #alt.degrees = 73  # For debug only.  Comment for normal use
    
    # Print the current time and date in Austin
    austin_time = current_time.astimezone(pytz.timezone('America/Chicago'))
    print(austin_time.strftime("%A"), austin_time.strftime("%m-%d-%Y %H:%M:%S %Z"))
    print("--------------------------------------")
    
    # Print altitude and azimuth of the ISS as integers
    print("ISS altitude: " + str(int(alt.degrees)) + "\u00b0")
    
    
    # Convert azimuth to cardinal compass direction
    compass_directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
    compass_index = round(az.degrees / (360. / len(compass_directions)))
    compass = compass_directions[int(compass_index) % len(compass_directions)]
    #print("Cardinal direction: " + compass)
    print(" ISS azimuth: " + str(int(az.degrees)) + "\u00b0" + " <" + compass + ">")
    
    # Check if the altitude is increasing or decreasing
    if last_altitude is not None:
        if alt.degrees > last_altitude:
            risefall = "RISING"
        elif alt.degrees < last_altitude:
            risefall = "FALLING"
        else:
            risefall = "STABLE"
            
    #risefall = "RISING" # For debug only.  Comment for normal use
            
    print("              " + risefall)
    last_altitude = alt.degrees  # Update the last altitude
    
    print(" ")

    # Display on LCD
    lcd_init()

    # Center the text for line 1
    alt_text = "Alt " + str(int(alt.degrees)) + " deg @"
    spaces = (LCD_WIDTH - len(alt_text)) // 2
    alt_text = " " * spaces + alt_text
    lcd_string(alt_text, LCD_LINE_1)

    # Center the text for line 2
    az_text = str(int(az.degrees)) + " deg <" + compass + ">"
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)

    # Flash LED if altitude is positive
    setup_led()
    if alt.degrees > 0:
        for _ in range(seconds_between_screens):  # Flash LED
            led_on()
            time.sleep(0.5)  # LED on for 0.5 seconds
            led_off()
            time.sleep(0.5)  # LED off for 0.5 seconds
    else:
        led_off()
        time.sleep(seconds_between_screens)
    
    # Date/Time on LCD briefly
    lcd_init()
    
    if alt.degrees > 0:
        led_on()  # LED on is ISS UP while Day/Time display

    # Center the text for line 1
    #alt_text = austin_time.strftime("%A")  # Show the DOW
    alt_text = risefall  # Rising or Falling?
    spaces = (LCD_WIDTH - len(alt_text)) // 2
    alt_text = " " * spaces + alt_text
    lcd_string(alt_text, LCD_LINE_1)

    # Center the text for line 2
    az_text = austin_time.strftime("%H:%M:%S %Z")  # Show the local time
    spaces = (LCD_WIDTH - len(az_text)) // 2
    az_text = " " * spaces + az_text
    lcd_string(az_text, LCD_LINE_2)   
    
    # Flash LED if altitude is positive
    setup_led()
    if alt.degrees > 0:
        for _ in range(seconds_between_screens):  # Flash LED
            led_on()
            time.sleep(0.5)  # LED on for 0.5 seconds
            led_off()
            time.sleep(0.5)  # LED off for 0.5 seconds
    else:
        led_off()
        time.sleep(seconds_between_screens)
    
    time.sleep(0.1)  # tiny delay before cleanup to ensure the LED is off
    GPIO.cleanup()
-----

Tuesday, January 23, 2024

WS2812B LED Fireworks Simulator


-----

WS2812B LED strips are pretty cool.  They are string of individually addressable RGB LEDs.  This allows control of the color and brightness of each LED.

Fireworks are also pretty cool.  On the downside they can be dangerous, scare wildlife, start fires, be expensive, illegal, etc.   So, until we can afford our own fleet of drones we settled on this alternative.  Like many projects, we stand on the shoulders of giants mentioned in the Ardunio source code below.  Our main issue with their code was the effect was never changing so we improved mainly on that aspect; a few other things as well.

----

Result:

-----

The hardware is an ESP8266 and a WS2812B LED-strip with 300 LEDs (16.5 feet).  We wanted to use a Ardunio Nano (because we had one), but due to the amount of memory needed to define the arrays for the 300 LEDs we went with an ESP8266 (because we had one).

-----
You are also going to need a big pole.

 



-----

/*  
 *  LED Fireworks Simulator
 *  WhiskeyTangoHotel.Com
 *  JAN 2024
 *
 *  To vary the effect experiments randomizing variables
 *  within acceptable limits was done.  Otherwise the effect
 *  just looks to same 'shot after shot'.  Other mods as well
  *
 *  Leverage from https://www.Daniel-Westhof.de and
 *  https://www.anirama.com/1000leds/1d-fireworks/
 *
 *  Hardware:  
 *  HiLetgo 1PC ESP8266 NodeMCU CP2102 ESP-12E Development Board and a
 *  WS2812B LED-strip with 300 LEDs (16.5 feet).
 */
 
#include <FastLED.h>
#define NUM_LEDS 300
#define DATA_PIN 5  // Labeled a D1 on the board.
#define LED_PIN 2  // This is the BLUE LED on board
#define NUM_SPARKS NUM_LEDS/2  // OG: NUM_LEDS/2
 
CRGB leds[NUM_LEDS]; // sets up block of memory
 
float sparkPos[NUM_SPARKS];
float sparkVel[NUM_SPARKS];
float sparkCol[NUM_SPARKS];
float flarePos;
float gravity = -.008; // m/s/s
int launch_delay; // we later randomize seconds between launches
 
void setup() {
  Serial.begin(115200);
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
  pinMode(LED_PIN, OUTPUT);
}
 
void loop() {   
  // Delay untill next launch. Blink BLUE on board LED
  Serial.println("  ");
  launch_delay = int(random(5,30)); // Min>=5.  Randomize secs between BOOMs.  
  //launch_delay = 5;   
  for (int i = (launch_delay - 5); i > 0; i--) {
    Serial.println(String(i + 5) + " seconds to launch...");
    digitalWrite(LED_PIN, LOW);  // ON
    delay(500);
    digitalWrite(LED_PIN, HIGH);  // OFF
    delay(500);    
  }

  // Slower timer done .  Fast blink for ~5 seconds to warn of BOOM
  Serial.print("5 seconds to launch!!!");
  for (int i = 50; i > 0; i--) {
    Serial.print(".");
    digitalWrite(LED_PIN, LOW);  // ON
    delay(50);
    digitalWrite(LED_PIN, HIGH);  // OFF
    delay(50);
  }

  Serial.println(".");
  Serial.print("BOOM...");
  digitalWrite(LED_PIN, LOW); // LED ON
 
  // send up flare
  flare();
  digitalWrite(LED_PIN, HIGH); // LED OFF
 
  // explode
  explodeLoop();
}
 

void flare() {
  flarePos = 0;  // 0
  // flareVel is how hight the BOOM is.  2.2 is max height
  float flareVel = float(random(180, 215)) / 100; // Start: (180, 195)) / 100; trial and error to get reasonable range
  Serial.println(" with flare height of " + String((flareVel*100)/2.2) + "%");  // How high is the BOOM?
  float brightness = 5;   // OG: 1
 
  // initialize launch sparks
  int blast_base = random(5,20);  // number of sparks at blast base.
  for (int i = 0; i < blast_base; i++) {   // OG: int i = 0; i < 5; i++  
    sparkPos[i] = 0; sparkVel[i] = (float(random8()) / 255) * (flareVel / 2); // OG: (float(random8()) / 255) * (flareVel / 5); the / xx); is control value for BURST
    sparkCol[i] = sparkVel[i] * 1000; sparkCol[i] = constrain(sparkCol[i], 0, 255);
    //Serial.println(String(i) + "   " + String(sparkVel[i]) + "  " + String(sparkCol[i]));
  }  

  // launch
  while (flareVel >= -.2) {   // OG: flareVel >= -.2  when to explode after peak BOOM.  Bigger neg val means more fall before sparks
    // sparks
    for (int i = 0; i < blast_base; i++) {   // OG: int i = 0; i < 5; i++  
      sparkPos[i] += sparkVel[i];
      sparkPos[i] = constrain(sparkPos[i], 0, 120);
      sparkVel[i] += gravity;
      sparkCol[i] += -.8;
      sparkCol[i] = constrain(sparkCol[i], 0, 255);
      leds[int(sparkPos[i])] = HeatColor(sparkCol[i]);
      leds[int(sparkPos[i])] %= 50; // reduce brightness to 50/255
    }
   
    // flare
    leds[int(flarePos)] = CHSV(0, 0, int(brightness * 255));
    FastLED.show();
    delay(5);
    FastLED.clear();
    flarePos += flareVel;
    flarePos = constrain(flarePos, 0, NUM_LEDS-1);
    flareVel += gravity;
    brightness *= .99; // OG = .98
  }  // while (flareVel >= -.2)
}  // end void flare
 
void explodeLoop() {
  int nSpark_var = random(2, 10);  // Bigger number is less BOOM sparks
  int nSparks = flarePos / nSpark_var; // OG: nSparks = flarePos / 2
  //Serial.println(String(nSparks));

   
  // initialize sparks
  for (int i = 0; i < nSparks; i++) {
    sparkPos[i] = flarePos; sparkVel[i] = (float(random(0, 20000)) / 10000.0) - 1.0; // from -1 to 1
    sparkCol[i] = abs(sparkVel[i]) * 500; // set colors before scaling velocity to keep them bright
    sparkCol[i] = constrain(sparkCol[i], 0, 255);
    sparkVel[i] *= flarePos / NUM_LEDS; // proportional to height
  }
  sparkCol[0] = 255; // OG: 255  This will be our known spark
  float dying_gravity = gravity;
  float c1 = random(80,130);  // OG: 120
  float c2 = random(1,30);   // OG: 50
  //Serial.println("c1 is: " + String(c1));
  //Serial.println("c2 is: " + String(c2));
    
 
  while(sparkCol[0] > c2/128) { // OG: (sparkCol[0] > c2/128)  As long as our known spark is lit, work with all the sparks
    int decay_rate = (random(0,50));  // Slow to decay the blast sparks.  (0,50) seems right. Bigger is slower.  OG: delay(0);
    delay(decay_rate);
    //Serial.println(String(decay_rate));
    FastLED.clear();
    
    for (int i = 0; i < nSparks; i++) {   
      sparkPos[i] += sparkVel[i];
      sparkPos[i] = constrain(sparkPos[i], 0, NUM_LEDS-1);
      sparkVel[i] += dying_gravity;
      sparkCol[i] *= .975;
      sparkCol[i] = constrain(sparkCol[i], 0, 255); // RED cross dissolve. OG: constrain(sparkCol[i], 0, 255);
      
      if(sparkCol[i] > c1) { // fade white to yellow
        leds[int(sparkPos[i])] = CRGB(random(0,255), random(200,255), (255 * (sparkCol[i] - c1)) / (255 - c1));  // OG: CRGB(255, 255, (255 * (sparkCol[i] - c1)) / (255 - c1));
      }
      else if (sparkCol[i] < c2) { // fade from red to black
        leds[int(sparkPos[i])] = CRGB((random(200,255) * sparkCol[i]) / c2, 0, 0); // OG: CRGB((255 * sparkCol[i]) / c2, 0, 0);
      }
      else { // fade from yellow to red
        leds[int(sparkPos[i])] = CRGB(random(0,255), (random(200,255) * (sparkCol[i] - c2)) / (c1 - c2), 0);  // OG: CRGB(255, (255 * (sparkCol[i] - c2)) / (c1 - c2), 0);
      }      
    }
    dying_gravity *= .99; // OG: dying_gravity *= .99;  As sparks burn out they fall slower
    FastLED.show();    
  }  // end while(sparkCol)
 
  delay(5);
  FastLED.clear();
  delay(5);  
  FastLED.show();
  Serial.println("Effect Complete!!!");
} // end void explodeLoop() 

-----

Sunday, June 11, 2017

Measuring Fidget Spinner RPM via TEK MDO4000 Oscilloscope


-----
How fast can a Fidget Spinner spin?  Well, it depends on what spins it.  Let's use compressed air.


That sounds fast, it's actually scary to hold!!!  But how fast?
-----
To make the measurement we used a simple LED as a sensor.  A bright light shines down onto the LED.  The arms of the Fidget Spinner block the light as they move over the LED creating electrical pulses. 
-----
The sensor LED is connected to a Tektronix MDO4104C to measure the pulses.  The scope (along with a few thousand other features) allows an engineer to quickly study waveform measurements with cursors.  After a little math we get our answer; well over 10,000 RPM.
That's fast!
-----

Sunday, September 18, 2016

Keep Tabs on the Devices Connected to Your LAN

-----
THE PROBLEM:
Like most people, we have tons of things connected via WiFi to our router.  Be it AppleTVs, Rasberry PIs, Amazon Echos, weather stations, overhead airplane trackers, security cameras, etc. etc. etc....  The problem arises when you think and expect a device to "work" and it doesn't.  Is it the device? Is it the service being accessed? VNC server not running?  Most of the time "all is well", but if it isn't it can be frustrating.  Who knew the security camera was down?  Why can't I VNC into that Raspberry PI?  Most of us have been there and this project helps solve the problem.
------
PROBLEM SOLVED:
We had in the 'warehouse'  a Raspberry PI Zero (mainly because it was cheap so why not) and a BLINKT (who doesn't like flashy light things) doing nothing and just waiting for a project.  Setting up the gear to monitor our LAN seemed perfect for the pair.

The solution is pretty straight forward: Get the RaspBerry PI Zero on your LAN via WiFi.  Plug in the BLINKT module.  Every thirty seconds or so send a ping to an IP of a device on your network.
If the device IP ping is good then
     - flash the BLINKT green.
If the device IP ping is bad then
     - retest the device just to make certain the failure wasn't an anomaly.
     - two failed pings back to back then flash the BLINKT red and log the failure to a Google Drive Sheet.
-----
RESULTS:

The rig works perfectly.  We can check the Google Drive Sheet or the BLINKT quickly see if anything on the LAN is down.  We use IFTTT.com to log the failures to the Google Drive Sheet.  We can just as easily use IFTTT.com to send out a text message, post a Tweet, call our phone, or a host of other IFTTT.com supported actions.  In short, it is pretty darn useful.

Here is what a sample output to the Google Drive Sheet looks like:
-----
SOURCE:
You will need an IFTTT.com and Google logons if you want to log ping status to a Google Drive Sheet.  Both are useful for a few hundred reasons, so if you don't have one then get one - they are free.

#!/usr/bin/env python

# LocalPing Testing/Tracking
# RasPI Zero with BLINKT module attached
# BLINKT scans back/forth with delay in mS = to CPU % use (45% = ,45 Secs)
#
# After a set delay (var = Ping_test_how_often)
# Checks if tracked IPs are up on the LAN. 
#      - If device ping passes, flash all BLINKT LEDs green.
#            - however, if there was a ping fail in the last xx (var = Ping_check) flash 1st LED as RED and rest as GREEN.
#      - If device ping Fails two pings back to back:
#            - use IFTTT Maker Channel to log the failed ping to a Google Drive sheet
#            - write the falied ping information to the terminal
#
# WhiskeyTangoHotel.Com - AUG2016

import os # allows for the ping calls
import time # for sleeps and localtime prints
import random # for random number generation
import psutil # for ping system calls
from blinkt import set_pixel, show, set_brightness, clear # https://shop.pimoroni.com/products/blinkt

# Set up for IFTTT triggering
import requests # needed to get https request to IFTTT web page
MAKER_SECRET_KEY = "xxxyyyzzz1234561890"  #this is your IFTTT Maker Channel secret key

PingSiteIP = [1,2,3,4,5,6,7,8,9,10,11]   # holds the IP or DNS name of the site being pinged
PingSiteName = [1,2,3,4,5,6,7,8,9,10,11]   # Friendly/Human name for the device

# Identify IPs that you want to track on your LAN
# PingSiteName[] can be anything. It is the "human" name for the IP
# Enter the 'x' part of the IP; the last number of the IP address without the '.'
IP_Prefix = "192.168.0."  # typically 192.168.0. or 192.168.1.  DON'T FORGET THE LAST '.'
PingSiteIP[1] = "11"
PingSiteName[1] = "RasPI(Upper)"
PingSiteIP[2] = "14"
PingSiteName[2] = "RasPI(Lower)"
PingSiteIP[3] = "20"
PingSiteName[3] = "Security(NW)"
PingSiteIP[4] = "23"
PingSiteName[4] = "Security(Drive)"
PingSiteIP[5] = "32"
PingSiteName[5] = "Security(SE)"
PingSiteIP[6] = "13"
PingSiteName[6] = "ElectricImp"

PingSiteIP[7] = "27)"
PingSiteName[7] = "RasPI(Airplanes)"
PinsSiteIP[8] = "49"
PingSiteName[8] = "RasPI(Weather)"
PingSiteIP[9] = "45"
PingSiteName[9] = "Printer(HP)"

Num_ping_sites = 9   # Make sure this matches the above number of site entries

Single_test_site = 1 # Must be 1; I did not want to start at array 0
Flash_on = .05       # Secs to keep ping status LEDs on
Flash_off = .2       # Secs to keep ping status LEDs off
Failed_ping = 0      # FLAG for if there is a failed ping in the last Ping_check trys then turn one green light red
Second_try_delay = 20    # seconds to wait to config falied ping on device (Yellow LEDs scan during this delay)
Ping_check = 15         # see comment above for Failed_Ping
Ping_check_counter = Ping_check # Counts Ping_check, decrements and resets at Zero
Ping_test_how_often = 10 # 10 is ~30 secs depending on delay from CPU load. Dont ping test on every cpu speed check
Every_x_times = Ping_test_how_often # decrementing counter for the Ping_test_how_often var
set_brightness(0.05) # 0.05 is lowest usable dim.  1.0 is full bright (the BLINKT is *really* bright if you want!)
LED_Delay = 0        # Controls the LED to LED scan speed. This is adjusted in the program after reading CPU % load
r_largest_wanted_value = 10  # these vars control the max amount of R, G, or B you desire. Also helps with more dimming.
g_largest_wanted_value = 10  # during the CPU load status LED back and forth scan on the BLINKT
b_largest_wanted_value = 255 # For example: 10, 10, 255 would mean favor BLUE hues.

Current_day = time.strftime("%d", time.localtime()) # what day of the month is it
New_day_dawned = Current_day # 1st run after midnight update the Google Sheet. Use to verify the prog has not stopped running
Failed_since_midnight = 0

print " "
print "Writing header information to Google Sheet..."
#Triggers the IFTTT Maker channel "local_ping" to create an information header
url = "https://maker.ifttt.com/trigger/local_ping/with/key/" + MAKER_SECRET_KEY + "?value1=" + "*************************"
res = requests.get(url)
#print str(res) + " is web request result. "   #used only for debug

print "Testing LEDs..."
print "---------------------------------------------"
#LED test and some delay for the IFTTT calls
for j in range (0,8):
    set_pixel(j, 30, 0, 0)
show()
time.sleep(Flash_on * j)
for j in range (0,8):
    set_pixel(j, 0, 30, 0)
show()
time.sleep(Flash_on * j)
for j in range (0,8):
    set_pixel(j, 0, 0, 30)
show()
time.sleep(Flash_on * j)   

for sheet_header in range (1, Num_ping_sites + 1):
    #this triggers the IFTTT Maker channel "local_ping" to update the google sheet.
    url = "https://maker.ifttt.com/trigger/local_ping/with/key/" + MAKER_SECRET_KEY + "?value1=" + "Tracking " + PingSiteName[sheet_header] + "&value2=" + IP_Prefix + str(PingSiteIP[sheet_header])
    res = requests.get(url)
    #print str(res) + " is web request result. "   #used only for debug
    print str(sheet_header) + ") " + PingSiteName[sheet_header] + " at " + IP_Prefix + str(PingSiteIP[sheet_header])
   
    #LED test and some delay for the IFTTT calls
    for j in range (0,8):
        set_pixel(j, 30, 0, 0)
    show()
    time.sleep(Flash_on * j)
    for j in range (0,8):
        set_pixel(j, 0, 30, 0)
    show()
    time.sleep(Flash_on * j)
    for j in range (0,8):
        set_pixel(j, 0, 0, 30)
    show()
    time.sleep(Flash_on * j)       

clear() # all LEDS off
   
url = "https://maker.ifttt.com/trigger/local_ping/with/key/" + MAKER_SECRET_KEY + "?value1=" + "*************************"
res = requests.get(url)
#print str(res) + " is web request result. "   #used only for debug
print " "
print "Done with test!!!"
localtime = time.asctime( time.localtime(time.time()) )
print localtime + ": Tracking begins for the devices listed..."
print " "

time.sleep(1) # let CPU% settle from prog load

while True:
    #LED delay is established randomly
    #random.seed() # seed off the CPU clock
    #r = random.random()
    #LED_Delay = (int((r * 10) + 1)) / 100.0
   
    #LED delay is equal to %CPU usage in mSec (45% = .45 Secs)
    LED_Delay = psutil.cpu_percent() / 100.0

    random.seed() # seed off the CPU clock
    r = random.random()
    red = int((r * r_largest_wanted_value) + 1)
   
    random.seed() # seed off the CPU clock
    r = random.random()
    green = int((r * g_largest_wanted_value) + 1)

    random.seed() # seed off the CPU clock
    r = random.random()
    blue = int((r * b_largest_wanted_value) + 1)   
   
    for i in range(8):       
        set_pixel(i , red, green, blue)
        show()
        time.sleep(LED_Delay)
        clear()

    #LED delay is established randomly
    #random.seed() # seed off the CPU clock
    #r = random.random()
    #LED_Delay = (int((r * 10) + 1)) / 100.0
   
    #LED delay is equal to %CPU usage in mSec (45% = .45mS)
    LED_Delay = psutil.cpu_percent() / 100.0

    random.seed() # seed off the CPU clock
    r = random.random()
    red = int((r * r_largest_wanted_value) + 1)
   
    random.seed() # seed off the CPU clock
    r = random.random()
    green = int((r * g_largest_wanted_value) + 1)

    random.seed() # seed off the CPU clock
    r = random.random()
    blue = int((r * b_largest_wanted_value) + 1)       

    for i in range(7, -1, -1):
        set_pixel(i , red, green, blue)
        show()
        time.sleep(LED_Delay)
        clear()
       
    # Local ping to see if all is up
    # But we don't want to check on every run so:
   
    if Every_x_times == 1: #  var is decremented in the else.  Check the ping
        Every_x_times = Ping_test_how_often
        if Single_test_site == Num_ping_sites:
            Single_test_site = 1
        else:   
            Single_test_site = Single_test_site + 1           
   
        if Ping_check_counter == 0:
            Ping_check_counter = Ping_check
            Failed_ping = 0
        else:
            Ping_check_counter = Ping_check_counter -1       
                   
        hostname = IP_Prefix + str(PingSiteIP[Single_test_site]) # OK, now the have selected one of our ping test sites
        # Ping test IP with os.system command
        response = os.system("ping -c 1 " + hostname + " >/dev/null")
        #print PingSiteName[Single_test_site] + " at IP " + str(PingSiteIP[Single_test_site])
        if response == 0:
            for    i in range (0,10): #flash all green
                for j in range (0,8):
                    if j ==0 and Failed_ping == 1:
                        set_pixel(j, 30, 0, 0)   # red pixel to show a recent failed ping
                    else:
                        set_pixel(j, 0, 30, 0)
                show()
                time.sleep(Flash_on)
                for j in range (0,8):
                    set_pixel(j, 0, 0, 0)
                show()
                time.sleep(Flash_off)               
        else: #response != 0:
            # Ping failed, but wait 5 seconds and try again.
            # Walk yellow LEDs to show re-test and to delay some before double checking the device ping
            for i in range(8):       
                set_pixel(i , 30, 30, 0)  # YELLOW LED walk
                show()
                time.sleep(Second_try_delay/16.0)     
                   
            clear()
            for i in range(7, -1, -1):
                set_pixel(i , 30, 30, 0)
                show()
                time.sleep(Second_try_delay/16.0)
                   
            response = os.system("ping -c 1 " + hostname + " >/dev/null") # let's check again to make certain the ping really failed.
            if response != 0:                                             # it take two back to back failed pings to consider that device down.
                Failed_since_midnight = Failed_since_midnight + 1  # resetable counted tracks the days failed ping count
               
                localtime = time.asctime( time.localtime(time.time()) )
                print localtime + ": Failed ping " + str(Failed_since_midnight) + " from " + PingSiteName[Single_test_site] + " at " + IP_Prefix + str(PingSiteIP[Single_test_site])
               
                #this triggers the IFTTT Maker channel "local_ping" to update the google sheet.
                url = "https://maker.ifttt.com/trigger/local_ping/with/key/" + MAKER_SECRET_KEY + "?value1=" + "FailCount[" + str(Failed_since_midnight) + "]:" + PingSiteName[Single_test_site] + "&value2=" + IP_Prefix + str(PingSiteIP[Single_test_site])
                res = requests.get(url)
                #print str(res) + " is web request result. "   #used only for debug
               
                for    i in range (0,10): #flash all red
                    Failed_ping = 1
                    for j in range (0,8):
                        set_pixel(j, 30, 0, 0)
                    show()
                    time.sleep(Flash_on)
                    for j in range (0,8):
                        set_pixel(j, 0, 0, 0)
                        show()
                    time.sleep(Flash_off)
    else: # if Every_x_times == 0:
        Every_x_times = Every_x_times - 1
       
    # Last thing is check if this is a new day.  If yes, this write status to Google sheet
    Current_day = time.strftime("%d", time.localtime())
    if Current_day != New_day_dawned:
        url = "https://maker.ifttt.com/trigger/local_ping/with/key/" + MAKER_SECRET_KEY + "?value1=" + ">>> Failed since midnight: " + str(Failed_since_midnight)
        res = requests.get(url)
        localtime = time.asctime( time.localtime(time.time()) )
        print " "
        print localtime + " >>> Failed since midnight: " + str(Failed_since_midnight)
        print " "
        time.sleep(1)
        Failed_since_midnight = 0
        New_day_dawned = Current_day       

   
-----
Give it a try.  It was been very very useful here.

Tuesday, September 1, 2015

Raspberry PI answers, "Is the internet up?"

 
It is a pretty commonly uttered question (sometimes loudly) around the home or office; "Is the internet up?"  As the goto IT Support Manager around the house this can get a little tiresome.  Most of the time the internet is up and it's a user or personal device problem that can be solved with a re-boot, re-load, re-etc.  Sometimes the internet is really down requiring a router and cable modem reboot or a call to the ISP.  Wouldn't a simple visual check that anyone could quickly understand be helpful?  Plus, as a bonus, provide some general network health statistics with a screen print and an hourly update to a Google Drive spreadsheet.

This project is amazingly useful and incredibly simple to duplicate.  If you already have a Raspberry PI it is very cheap to implement.
-----
What it does and how it works:
The Raspberry PI runs a Python program that test pings 10 internet sites that you to determine to provide the status of your internet connection.

      GREEN: 7 or more sites return a successful ping.
                      Green LED on.  Updates the stats.
  YELLOW: 6 to 4 sites return a successful ping.
                     Yellow LED on.  Updates the stats.
           RED: 3 or fewer sites return a successful ping.
                     Red LED on.  Updates the stats.

To make the program run faster and not burden the network with outgoing pings, all 10 sites are not pinged on each run.  The ping test rotates through the 10 sites.  If the selected site does fail the ping test, the program goes into "Deep Probe" and checks all 10 sites to determine the connection status.  "Deep Probe" runs are tracked for the status report that prints to the screen.

Each hour a status report is appended to a Google Drive spreadsheet via the Maker channel at IFTTT.Com.
-----
For a visual output we went with a device that anyone can identify with, a simple $8 toy traffic light from Amazon.  For $8 you get a lot.  The light is well build, looks great, and is a full 7" tall with three LEDs already installed.  It also comes with 12 little toy cars that you can give away; you don't need those.  Next remove the four screws at the bottom of the base.  You are going to see four wires that lead up to the three LEDs in the traffic light.  Cut those wires and free them from the little ON/OFF switch and microcontroller that was meant to control the blink patterns for the non-hacker.  Now you should only be left with four wires.  The LEDs are wired common athode (+).  The simple schematic below shows the hookup for each LED and their connection to the Raspberry PI.  Double check the wiring for your traffic light with a button cell battery to see what wire lights each LED.
 
-----
You don't need to open the back, but if you did it would look like this:

-----
After you wire in the toy traffic light it will look something like this:
----- 
Now you will need to load the Python source code provided below, but before you do, a few simple edits are required:
   - Find in the code where it says "# Identify ten PING 'friendly' sites".  In that section put in ten IP addresses or DNS names that are "ping friendly".  Not all sites allow you to ping them, so test ping them manually first from a command line.
   - Find the variable named "Delay_between_tests".  In the program it is set to 15 (for 15 seconds).  Adjust this if you want to test more or less frequently.
-----
That's about it, now run the program.  The RasPI will take the traffic light through an LED self test.  Then it will do an initial first run deep probe ping test on all ten of the test sites; the LEDs will self test during this as well.
-----
From then on the little toy traffic light will answer that all too common "Is the internet up?" question.  In addition, as the local IT Support Manager, you will have access to a running history in a Google Drive spreadsheet and the status output table below to impress your friends:

-----
Here is the Python code that makes it all work.  Of course, as mentioned above, you will need to supply your own list of ten ping friendly internet sites instead of "yourtestsite-1.com", etc.

# Is the Internet UP?  /    August 2015
#
# Project details at:
# http://whiskeytangohotel.com/internetup
#
# Using a Raspberry PI we ping a 10 IP addresses and light up a Red, Yellow, or Green LED
# to display the 'health' of our internet connection. 
#
# SEPT 3, 2015: Adding IFTTT.com Maker channel to log status to Google Drive each hour.
#
# At least 7 of 10 successful pings to light the Green LED
# 3 or fewer of 10 successful pings for Red LED
# Anything in between and light the Yellow LED
#
# Program does not ping all 10 sites each time.  For speed, etc. one of the sites
# is tested  at each run.  If that ping fails, 'deep probe' mode involkes and all sites are tested
# to determine latest Red/Yellow/Green status.
#
# As an output device we used a $7 toy stoplight from Amazon.com. Search:
# "Dazzling Toys Racer Cars and Traffic Set - 7" Traffic Light with 12 Pull Back 2" Racer Cars"
#
# In addition to the LED stop light status. The details below are screen printed on each run.
#
# Tue, 01 Sep 2015 05:03:32.  Update interval: 15 seconds.
# Status report: GREEN.  Runs: 1812.  DeepProbes: 6.  Runtime: 8:42:44.448047
# -----------------------------------------------------------------------------------
#        Last check:  UP      pingfriendlysite-1.com was UP 188 of 189 pings.  99% uptime.
# >>>Last check:  UP      pingfriendlysite-2.com was UP 189 of 189 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-3.com was UP 187 of 188 pings.  99% uptime.
#        Last check:  UP      pingfriendlysite-4.com was UP 187 of 188 pings.  99% uptime.
#        Last check:  UP      pingfriendlysite-5.com was UP 188 of 188 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-6.com was UP 188 of 188 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-7.com was UP 188 of 188 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-8.com was UP 188 of 188 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-9.com was UP 188 of 188 pings.  100% uptime.
#        Last check:  UP      pingfriendlysite-10.com was UP 185 of 188 pings.  98% uptime.
# ------------------------------------------------------------------------------------
#   
#     RED:     0 events.  Last event: none detected
#  YELLOW:     0 events.  Last event: none detected
#   GREEN:  1812 events.  Last event: Tue, 01 Sep 2015 05:03:32
#

from __future__ import division # don't round  interger division to  'zero'
import time 
import datetime # used for timestamp math
t0 = datetime.datetime.now()  # for timestamp math. t0 is the time the program run started
runtime = 0 # localtime minus t0 = total runtime of the program

# Set up for IFTTT triggering
import requests # needed to get https request to IFTTT web page
Current_hr = time.strftime("%H", time.localtime()) # use this var to update trigger IFTTT at each hour change
IFTTT_hr = "999" # we will only trigger IFTTT if this is diffent from Current_hr
MAKER_SECRET_KEY = "xxxx-my-maker-ifft-key-xxx"  #this is my IFTTT Maker Channel secret key

Delay_between_tests = 15 #in seconds. Delay is in last line of code
Loop_count = 0 # track how many times the Red/Yellow/Green status was updated.
Deep_probe = 0 # how many times was a deep probe required; Single_test_site ping test failed
Single_test_site = 0 # used to select non deep probe ping test.  Rotates thru the 10 IPs

Last_Red = "none detected"  # timestamps for when condition happened for screen report
Red_Event_Counter = 0       #also count number of time condition occured
Last_Yellow = "none detected"
Yellow_Event_Counter = 0
Last_Green = "none detected"
Green_Event_Counter = 0

import os # allows for the ping calls

import RPi.GPIO as GPIO  # We will be using the RasPI GPIO to control the three LEDss
GPIO.setwarnings(False) # suppress screen from showing GPIO errors

# to use Raspberry Pi board pin numbers
GPIO.setmode(GPIO.BOARD)
# set up GPIO output channel
# I/O 15 = RED
# I/O 18 = YELLOW
# I/O 22 = GREEN
# NOTE: LOGIC LOW TURNS ON THE LEDs (they are active LOW)
GPIO.setup(15, GPIO.OUT)
GPIO.setup(18, GPIO.OUT)
GPIO.setup(22, GPIO.OUT)

#Turn off all the LEDs
GPIO.output(15,GPIO.HIGH) 
GPIO.output(18,GPIO.HIGH)
GPIO.output(22,GPIO.HIGH)

SiteStatus = [1,2,3,4,5,6,7,8,9,10,11]  # Pass/Fail of the site ping
UpTally = [1,2,3,4,5,6,7,8,9,10,11]    # count succesful pings for each site
PingCount = [1,2,3,4,5,6,7,8,9,10,11]  # count how many times each site is pinged
PingSite = [1,2,3,4,5,6,7,8,9,10,11]   # holds the IP or DNS name of the site being pinged

# Identify ten PING 'friendly' sites
PingSite[1] = "pingfriendlysite-1.com"
PingSite[2] = "pingfriendlysite-2.com"
PingSite[3] = "pingfriendlysite-3.com"
PingSite[4] = "pingfriendlysite-4.com"
PingSite[5] = "pingfriendlysite-5.com"
PingSite[6] = "pingfriendlysite-6.com"
PingSite[7] = "pingfriendlysite-7.com"
PingSite[8] = "pingfriendlysite-8.com"
PingSite[9] = "pingfriendlysite-.9com"
PingSite[10] = "pingfriendlysite-10.com"

print "  " # print startime, self test, etc...
print time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
print "-------------------------"
print "LED Self Test in process..."
print "  "
print "Test Pings Sites are:"
print "-------------------------"
for checksite in range (1,11): #init some vars, display sites on screen during self test.
    UpTally[checksite] = 0 # if site is UP when tested then incremented
    PingCount[checksite] = 0 # if site is tested then increment
    SiteStatus[checksite] = "---"
    print checksite, PingSite[checksite]
   
# Self test the LEDs by blinking them.  Remember; active LOW.
st_delay = 0.20
for st in range (0, 5): 
    GPIO.output(15,GPIO.LOW) # Red
    time.sleep(st_delay)
    GPIO.output(15,GPIO.HIGH)
   
    GPIO.output(18,GPIO.LOW) # Yellow
    time.sleep(st_delay)
    GPIO.output(18,GPIO.HIGH)

    GPIO.output(22,GPIO.LOW) #Green
    time.sleep(st_delay)
    GPIO.output(22,GPIO.HIGH)

print "  "
print "Test pinging all sites for first run..."
print "  "

# get a 1st run baseline so the status printout looks normal/clean
LED_status = 0  # LED_Status keeps score of the successful pings on the test sites
for checksite in range (1,11):
    # All LEDs on during the 1st run pings; flash effect
    print "  "
    print "All LEDs on..."
    GPIO.output(15,GPIO.LOW)
    GPIO.output(18,GPIO.LOW)
    GPIO.output(22,GPIO.LOW)
   
    hostname = PingSite[checksite]
    PingCount[checksite] = PingCount[checksite] + 1
    response = os.system("ping -c 1 " + hostname)
    if response == 0:
        #hostname 'is up!'
        SiteStatus[checksite] = "UP"
        LED_status = LED_status + 1
        UpTally[checksite] = UpTally[checksite] + 1
    else:
        #hostname 'is down!'
        SiteStatus[checksite] = "DOWN"
       
    # All LEDs off; flash effect
    print "  "
    print "All LEDs off..."
    GPIO.output(15,GPIO.HIGH)
    GPIO.output(18,GPIO.HIGH)
    GPIO.output(22,GPIO.HIGH)
    time.sleep(.5) # little delay for the LED off

print "  "
print "Self Test complete..."
print "  "

while True: #Loop forever to check and display ping test status
    Loop_count = Loop_count + 1
    # Only test ping all sites if the one site tested sites fails the ping
    # Here we test ping a single site.  Next time we ping test the next site on our list of 10.
    if Single_test_site < 10:  # single test site selection
        Single_test_site = Single_test_site + 1
    else:
        Single_test_site = 1
       
    hostname = PingSite[Single_test_site] # OK, now the have selected one of our ping test sites
    print "Ping site: " + hostname        # so let's test ping it with os.system command
    print "  "
    time.sleep(1.5)
    PingCount[Single_test_site] = PingCount[Single_test_site] + 1
    response = os.system("ping -c 1 " + hostname)
    if response == 0:
        # tested site is up.  all is fine.  don't deep probe, but update the tally stats
        SiteStatus[Single_test_site] = "UP"
        UpTally[Single_test_site] = UpTally[Single_test_site] + 1
        Led_status = 10 # insure GREEN condition
    else: # If selected site is down, so now we call for a vote by testing all sites. Let's deep probe.
        print "  "
        print "Selected site failed ping test.  Intiating deep probe..."
        print "  "
        Deep_probe = Deep_probe + 1
        LED_status = 0
        for checksite in range (1,11):
            hostname = PingSite[checksite]
            PingCount[checksite] = PingCount[checksite] + 1
            response = os.system("ping -c 1 " + hostname)
            if response == 0:
                #hostname 'is up!'
                SiteStatus[checksite] = "UP"
                LED_status = LED_status + 1
                UpTally[checksite] = UpTally[checksite] + 1
            else:
                #hostname 'is down!'
                SiteStatus[checksite] = "DOWN"
        time.sleep(.1)
   
    #Adjust the LED status light and screen print a report
           
    if (LED_status >= 7): #Hurray, looking good. Turn on Green LED.  All others off
        GPIO.output(15,GPIO.HIGH) #Red OFF
        GPIO.output(18,GPIO.HIGH) #Yellow OFF
        GPIO.output(22,GPIO.LOW) #Green ON
        Status_Color = "GREEN"
        Last_Green = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
        Green_Event_Counter = Green_Event_Counter + 1
       
    if 3 < LED_status < 7:  #Things are shakey.  Yellow LED on.  All others off
        GPIO.output(15,GPIO.HIGH) #Red OFF
        GPIO.output(18,GPIO.LOW) #Yellow ON
        GPIO.output(22,GPIO.HIGH) #Green OFF   
        Status_Color = "YELLOW"   
        Last_Yellow = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
        Yellow_Event_Counter = Yellow_Event_Counter + 1
       
    if (LED_status <= 3): #Oh no, looking bad. Turn on Red LED.  All others off
        GPIO.output(15,GPIO.LOW) #Red ON
        GPIO.output(18,GPIO.HIGH) #Yellow OFF
        GPIO.output(22,GPIO.HIGH) #Green OFF
        Status_Color = "RED"       
        Last_Red = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
        Red_Event_Counter = Red_Event_Counter + 1
   
    os.system('clear')  # clear the screen for the report
    print "  "
    print time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + ".  Update interval: " + str(Delay_between_tests) + " seconds."
    runtime = datetime.datetime.now() - t0
    print "Status report: " + Status_Color + ".  Runs: " + str(Loop_count) + ".  DeepProbes: " + str(Deep_probe) + ".  Runtime: " + str(runtime)
    print "--------------------------------------------------------------------------"
    for checksite in range (1,11):
        if Single_test_site == checksite:
            LastCheck = ">>>Last check:"
        else:
            LastCheck = "   Last check:"
    Red_print =    "     RED:" + "%6s" % str(Red_Event_Counter)    + " events.  Last event: " + Last_Red
    Yellow_print = "  YELLOW:" + "%6s" % str(Yellow_Event_Counter) + " events.  Last event: " + Last_Yellow
    Green_print =  "   GREEN:" + "%6s" % str(Green_Event_Counter)  + " events.  Last event: " + Last_Green
  
    print "--------------------------------------------------------------------------"
    print "  "
    print Red_print
    print Yellow_print
    print Green_print
    print " "
  
    # At each hour trigger IFTTT and update Google Drive Spreadsheet.  Will always trigger on 1st run
    Current_hr = time.strftime("%H", time.localtime())
    if (Current_hr != IFTTT_hr):
        IFTTT_hr = Current_hr
        url = url = "https://maker.ifttt.com/trigger/trigger_stat/with/key/" + MAKER_SECRET_KEY + "?value1=" + Red_print + "&value2=" + Yellow_print + "&value3=" + Green_print
        res = requests.get(url)
        #print str(res) + " is web request result for BT device: "   #used only for debug
        time.sleep(5)

    time.sleep(Delay_between_tests) #wait some before next ping test
 -----
If you're still with us, thanks and if you duplicate the build let us know!