Sunday, February 20, 2022

Simpson 260 VOM 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 go to 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.  Rarely is the internet 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? 

-----

I recently found my father's old Simpson 260 meter.  He let me borrow it anytime I wanted with the warning of "DON'T COOK IT!".   My memory recalls only using it for continuity and batteries which is good because I did not have a clue as to what would "cook it".  I decided to put this heirloom to use as an internet monitor.  This project is amazingly useful and simple to duplicate.

-----

The rig uses an ESP8266 to ping different servers.   The ping time (in mS) is displayed on the Simpson 260 and 'percent of full scale'.   In other words a ping of 73mS would be 73% of full scale on the Simpson 260.    We set the source code (see below) to cycle through ten servers and show the ping result every 15 seconds.

-----

If the LAN is down or a ping error is detected the meter "wags" back and forth 5 times and tries another server.   The message to the house is, "If the needle ain't wagging back and forth then your problem ain't with the internet connection!"

-----

Upload the Arduino IDE based source code below to the ESP8266.  Connection to the Simpson 260 is easy. 
 

-----

/*
 *  A vintage Simpson 260 meter to shows network PING times
 *  and if there is a connection to the internet.
 *  
 *  Deflect the Simpson meter from 0-100% of the 2.5V full scall
 *  based on ping times of servers.
 *  
 *  10mS = 10% of full scale.   
 *  45mS = 55% of full scale.
 *  73mS = 73% if full scale.
 *  XXmS = XX% of full scale, etc....
 *  Anything over 100ms is consider terrible and just maxes to 100%
 *  
 *  A bad ping (site not found, network down, etc) will 'wag' the meter
 *  back and forth from 0% to 100% five times then try next ping site.
 *
 *  ESP8266 NodeMCU Board BAUD 115200
 *  HiLetgo 1PC ESP8266 NodeMCU CP2102 ESP-12E Development Board from Amazon
 *  
 *  Full documetation at:
 *  WhiskyTangoHotel.Com    
 *  
 *  FEB2022
 *  
 */

#include <ESP8266WiFi.h>
#include <Pinger.h>

const char* ssid     = "Virus-2.4";      // These vars are your private WIFI
const char* password = "zenakelsocats";  // connection information

// Define some 'ping friendly' sites.   ARRAY starts a 0
String PingSite[] = {  
  "whiskeytangohotel.com",
  "google.com",
  "yahoo.com",
  "bing.com",
  "abc.com",
  "cbs.com",
  "cnn.com",
  "apple.com",
  "pingler.com",
  "mailinator.com"
};  // end ping array define

int Number_of_Ping_Sites = 10;   // counted from the list above
int Secs_Between_Pings = 15;
float Min_Ping_Result = 999;
const int PINGOUT = 2;   // Drives the S260. Blue onboard LED and ~D4
Pinger pinger;

void setup() {
  analogWrite(PINGOUT, 0);
  delay(50);
 
  Serial.begin(115200);
  delay(100);

  // Connect to the WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

// For the ping dot h features https://github.com/bluemurder/esp8266-ping
  pinger.OnEnd([](const PingerResponse& response)
  {
    // Print time information
    if(response.TotalReceivedResponses > 0)
    {
      Serial.printf("Approximate round trip times in milli-seconds:\n");
      Serial.printf(
        "    Minimum = %lums, Maximum = %lums, Average = %.2fms\n",
        response.MinResponseTime,
        response.MaxResponseTime,
        response.AvgResponseTime);
        Min_Ping_Result = response.MinResponseTime;
    }
    
    // Print host data
    Serial.printf("Destination host data:\n");
    Serial.printf(
      "    IP address: %s\n",
      response.DestIPAddress.toString().c_str());
    if(response.DestMacAddress != nullptr)
    {
      Serial.printf(
        "    MAC address: " MACSTR "\n",
        MAC2STR(response.DestMacAddress->addr));
    }
    if(response.DestHostname != "")
    {
      Serial.printf(
        "    DNS name: %s\n",
        response.DestHostname.c_str());
        
        Serial.println("Minimum ping was: " + String(Min_Ping_Result) + "mS." + " Meter to " + String(int(Min_Ping_Result)) + "% of full scale.");
        Serial.println("Delay to next ping is " + String(Secs_Between_Pings) + " seconds...");
        Serial.println("---------------------");
    }
    return true;        
  });  // end ping features

  //Self Test the meter by moving the meter full scale
  // Increase meter value and on board LED brightness
  for(int dutyCycle = 0; dutyCycle < 200; dutyCycle++){   
    // changing the LED brightness with PWM;
    analogWrite(PINGOUT, dutyCycle);
    Serial.println(String(dutyCycle) + " increasing meter self test...");
    //analogWrite(PINGOUT, testval);
    delay(10);
  }   // end meter increase
 
  // Decrease meter value and on board LED brightness
  for(int dutyCycle = 200; dutyCycle > 0; dutyCycle--){
    // changing the LED brightness with PWM
    analogWrite(PINGOUT, dutyCycle);
    Serial.println(String(dutyCycle) + " decreasing meter self test...");
    //analogWrite(PINGOUT, testval);
    delay(10);
  }  // end meter decrease  
  Serial.println("Self test complete!!!");
  Serial.println("---------------------");
 
}   // end void setup

// dutytCycle/2 = ~ the % of 2.5V scale on S260
//    0 dutytCycle =
//   50 dutytCycle = 28%
//  100 dutytCycle = 53%
//  150 dutytCycle = 77%
//  200 dutytCycle = 100%

// Set S260 to +DC.  The + lead to D4.  Neg lead to GND

void loop() {    // loop until the Cowboys win a Super Bowl

  for (int i = 0; i <= (Number_of_Ping_Sites - 1); i++) {  // don't always use the same PingSite; cycle them.
      Serial.println("PingSite[" + String(i) + "]: " + PingSite[i]);
            
      if(pinger.Ping(PingSite[i]) == false)
      {    
         Serial.println("Error during ping command.  Walk the meter 5 times.");
         // Walk the meter back and forth to symbol 'ping error' or network down    
          for (int walk = 0; walk<=4; walk++) {
              // Increase meter value and on board LED brightness
              for(int dutyCycle = 0; dutyCycle < 200; dutyCycle++){   
                // changing the LED brightness with PWM;
                analogWrite(PINGOUT, dutyCycle);
                Serial.println(String(dutyCycle) + " Showing FAIL increasing for: " + PingSite[i]);
                delay(10);
              }   // end fail meter increase
            
              // Decrease meter value and on board LED brightness
              for(int dutyCycle = 200; dutyCycle > 0; dutyCycle--){
                // changing the LED brightness with PWM
                analogWrite(PINGOUT, dutyCycle);
                Serial.println(String(dutyCycle) + " Showing FAIL decreasing for: " + PingSite[i]);
                delay(10);
              }  // end fail meter decrease
          } // end for fail meter back forth walk          
      }  // end if pinger.Ping       

      // Write Ping value to the meter.  Low is better.
      // We basically make percent of full scale equal the ping in mS, ie; 45mS = 45%...
      // Anything over a 100mS is a crappy ping so we make 100mS (100% of scale)
      if (Min_Ping_Result > 100) { Min_Ping_Result = 100; }
      
      analogWrite(PINGOUT, Min_Ping_Result * 2);  // move to meter to display the ping value
      delay(Secs_Between_Pings * 1000);   // delay for next ping    
  }  // end for/next to cycle the PingSites             
}  // end loop until the Cowboys win the Super Bowl
-----

Monday, February 7, 2022

Retirement Clock Goes Digital

 

-----

We overheard in a conversation recently something like: "Now that I'm retired I really only focus on what day of the week it is and the general time of day."  We then discovered this awful looking Day of the Week Clock and the vision of a more modern version came to mind.

-----

The rig is based around this MakerFocus ESP32 Development Board mainly because it works with the Arduino IDE and has WiFi with a display on board.   The code (source below) gets it's time/date from a NTP server and then simply figures out and displays the day of the week and what percentage of the day has past.  Evidently this is all the critical information needed to guide the non-working through their day.

-----

To dress up the look a case was 3D printed.  After seeing the result we needed to celebrate with a beer.   As shown in the video below it was a bit early in the day for a drink, but as the ol' saying goes "It's 71% somewhere!".

-----

Retirement Clock Source Code:

/*
 * HelTec Automation(TM) ESP32 Series Dev boards OLED "Retirement Clock"
 * Adruino IDE Board Setting: WiFi Kit32, Disabled, 240MHz, 921600, None
 *
 * "Retirement Clock" shows only Day of Week and percentage time left in the day.
 *  
 * FEB 2022
 * Search whiskeytangohotel.com for project details.
 *
*/

#include "Arduino.h"
#include "heltec.h"

#include <TimeLib.h>
#include <WiFi.h>
#include <WiFiUdp.h>

const char ssid[] = "xxxxxxx";  //  your network SSID (name)
const char pass[] = "xxxxxxx";       // your network password
long Sync_Delay = 60000;  //  60000 is sync every ten minutes,  We aren't going for precision here. LOL

int DOWNUM = 0;     //  DOWNUM from NTP (expect 0 - 6)
String DOW = "DOW";  // DOWNUM to a Day of Week String
int hourNUM = 0;    //  hour() value from NTP
float minuteNUM = 0;   // minute() value from NPT

// NTP Servers:
static const char ntpServerName[] = "pool.ntp.org";
const int timeZone = 0;     // leave as 0 for UTC.  We do the offset math in void setup

WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);

void setup() {
  //NTP setup
  Serial.begin(115200);
  while (!Serial) ; // Needed for Leonardo only
  delay(250);
  Serial.println("TimeNTP");
  Serial.print("Connecting to... ");
  Serial.println(ssid);
  WiFi.disconnect();
  WiFi.mode(WIFI_MODE_STA);
  WiFi.begin(ssid, pass);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.print("IP number assigned by DHCP is: ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
//  Serial.println(Udp.localPort());
  Serial.println("waiting for sync... ");
  setSyncProvider(getNtpTime);
  setSyncInterval(Sync_Delay);  //   defines how often to check NTP time

  // Hetec setup
  Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, true /*Serial Enable*/);
  Heltec.display->flipScreenVertically();
  Heltec.display->setFont(ArialMT_Plain_24);
} // end setup link

time_t prevDisplay = 0; // when the digital clock was last displayed timer

void loop() {   // main program loop
  if (timeStatus() != timeNotSet) {
    if (now() != prevDisplay) { //update the display only if time has changed
      prevDisplay = now();
    }
  }

  // Parse time info and send info to serial monitor (for debug)
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(".");
  Serial.print(month());
  Serial.print(".");
  Serial.print(year());
  Serial.print(" : ");

  if (int(year()) < 2022) {
     Serial.print(String(int(year())) + " is the NTP server year.  That aint right, so try again!!!");
     Serial.println();      
     setup();
  }   // the NTP server return something 'wierd' (probably 1970) so try again  

  //Covert Time in percent of day completed.  Midnight Local = 0%, Noon Local = 50%  
  minuteNUM = minute();
  hourNUM =   int(hour());  
  // manually adjust hourNUM and minuteNUM for debug
  // hourNUM =  5;    // valid values are INTERGERS 0 thru 24
  // minuteNUM = 49;    // valid values are INTEGERS 0 thru 60  

  minuteNUM = minuteNUM / 60;    // covert minutes to 'fraction'  
  hourNUM = hourNUM - 6;   // hourNUM is in UTC.   Adjust to local.   -6 = CST, -5 = CDT, 0 = UTC
  DOWNUM = int(weekday());
   
  // Calculate percentage of the day pasted into 'progress'
  int progress = round(   (  ((hourNUM + minuteNUM))   /  24   )  * 100    );
  if (progress < 0) {   // it's past UTC midnight and the timezone offset created a negative %
    progress = 100 + progress;
    DOWNUM = DOWNUM -  1;
  }  //end progress < 0

  // Convert DOWNUM into printable String.  Sunday is 1
  if (DOWNUM  == 1) { DOW = "SUN"; }
  if (DOWNUM  == 2) { DOW = "MON"; }
  if (DOWNUM  == 3) { DOW = "TUE"; }
  if (DOWNUM  == 4) { DOW = "WED"; }
  if (DOWNUM  == 5) { DOW = "THU"; }
  if (DOWNUM  == 6) { DOW = "FRI"; }
  if (DOWNUM  == 7) { DOW = "SAT"; }

  Serial.print(String(int(weekday())) + " is " + DOW);
  Serial.println();   
  Serial.print(String(hourNUM) + " + " + String(minuteNUM) + " = " + String(progress) + "%");
  Serial.println();
 
  // clear the display
  Heltec.display->clear();

  // Draw the Precent of Day pasted onto progress bar
  Heltec.display->drawProgressBar(0, 0, 127, 20, progress);

  // Label the Day Of Week and percentage as String
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(60, 31, DOW + " " + String(progress) + "%");
  // Write buffer to Heltec display
  Heltec.display->display();
 
  delay(5000); // wait some.  we are in no hurry to update the screen
}  // end void loop main program


void printDigits(int digits)
{
  // utility for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

-----