Coronameter

From Alnwlsn - Projects Repository
Revision as of 22:17, 14 March 2020 by Alnwlsn (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

As Michigan gets it's first cases of COVID-19 and events, schools, and businesses close up, this global pandemic starts to feel a lot more real. To stay properly panicked, I decided to re-purpose my old temperature display from college as a dedicated screen showing what the score is.

CDC and WHO update slowly, so for country and world stats I used a mirror of the John Hopkins interactive map you might have seen around, but in an easier to work with JSON format. For State data, I use the michigan.gov site, which I found to update more quickly. I also decided to include the Dow and S&P indexes, as well as Clorox, just for the meme. That data comes directly from Yahoo Finance (not the discontinued API, but scraped off their website).

Screen

Hardware

The hardware is nothing more than a ST7920 LCD wired to an ESP8266. The screen runs at 5V, so there is a logic level converter thrown in, too. This screen used to be a display for my indoor outdoor temperature thingy, is crudely soldered, and had no case. First order of business was to design and 3D print a small plastic enclosure for it. After wiring up a 5V wall adapter, the hardware is done.

Software

When it was a temperature display, all the fetching of data was done on the ESP8266 itself. For the new system, I decided to just make the display dump the contents of raw UDP packets. That way, I can do the hard work in an external program (like Python) and send it over to the screen when done. I did the same thing with the Winter Camp Mission timer setup with the Nixie Clocks, and a character VFD hooked to an 8266, and it worked very well.

The screen is driven using u8g2lib, and since the ST7920 doesn't have a built in text mode with this library, it was up to me to pick out a font and position the characters on the screen. U8g2lib will draw the characters graphically. I wound up with 21 columns and 7 rows, 147 characters total. On a screen this size, this makes for large and clearly readable text, even at a distance.

#include <Arduino.h>
#include <U8g2lib.h>

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>

const char* ssid = "ssidname";    //  your network SSID (name) 
const char* pass = "password";   // your network password

unsigned int localPort = 4212;
WiFiUDP Udp;

U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 12, /* data=*/ 13, /* CS=*/ 4, /* reset=*/ 14);

char zbuffer[32];

char sbuf[148];

unsigned int millistimer=0;

void setup(void) {
  u8g2.begin();
  Serial.begin(74880);

  u8g2.setFont(u8g2_font_profont11_tr);
  u8g2.setFontRefHeightExtendedText();
  u8g2.setDrawColor(1);
  u8g2.setFontPosTop();
  u8g2.setFontDirection(0);

  sprintf(zbuffer, "Starting");
  u8g2.drawStr(0, 0, zbuffer);
  u8g2.sendBuffer();

  WiFi.mode(WIFI_STA);
  WiFi.softAPdisconnect (true);
  WiFi.begin(ssid, pass);
  
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
  }
  
  Serial.println(WiFi.localIP());
  u8g2.clearBuffer();
  sprintf(zbuffer, "OK: %d.%d.%d.%d",WiFi.localIP()[0],WiFi.localIP()[1],WiFi.localIP()[2],WiFi.localIP()[3]);
  u8g2.drawStr(0, 0, zbuffer);
  u8g2.sendBuffer();

  Udp.begin(localPort);
  ArduinoOTA.setHostname("graphomatic-udp");
  ArduinoOTA.begin();
 
  millistimer=millis();
}

void loop(void) {
  ArduinoOTA.handle();
  int packetSize = Udp.parsePacket();
  if(packetSize){
    Udp.read(sbuf, sizeof(sbuf));
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_profont11_tr);
    u8g2.setFontRefHeightExtendedText();
    u8g2.setDrawColor(1);
    u8g2.setFontPosTop();
    u8g2.setFontDirection(0);
    byte x=0;
    byte y=0;
    byte c=0;
    while(sbuf[c]!=0&&c!=255){
      if(sbuf[c]>31&&sbuf[c]<127){
        u8g2.drawGlyph(x*6+1,y*9-1,sbuf[c]);
      }
      if(sbuf[c]==10){y++; x=255;}
      x++;
      if(x==21){x=0; y++;}
      c++;
    }
    u8g2.sendBuffer();
  }
       
}

Getting the data

Next, I need to get the stats, format it for the screen, and send it over. Python makes this easy to do, even though I hadn't done this before.

Some of the numbers I get using a regex on the raw html I get when doing a request to a webpage. Another site I pick from that shows the COVID-19 world and country stats gives a JSON response, which Python also has good tools to work with.

import requests
import json
import re
import socket
import datetime
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while 1:
    try:
        rW = requests.get('https://covid19.mathdro.id/api')
        wc = rW.json().get('confirmed').get('value')
        wd = rW.json().get('deaths').get('value')
        wr = rW.json().get('recovered').get('value')
    except:
        wc=0
        wd=0
        wr=0

    try:
        rA = requests.get('https://covid19.mathdro.id/api/countries/USA')
        ac = rA.json().get('confirmed').get('value')
        ad = rA.json().get('deaths').get('value')
        ar = rA.json().get('recovered').get('value')
    except:
        ac=0
        ad=0
        ar=0
        
    try:
        rM = requests.get('https://covid19.mathdro.id/api/countries/USA/confirmed')
        rMj = [x for x in rM.json() if x.get("provinceState") == "Michigan"][0]
        #mc = rMj.get('confirmed')
        md = rMj.get('deaths')
        mr = rMj.get('recovered')
    except:
        md=0
        mr=0

    try:
        rD = requests.get('https://www.michigan.gov/Coronavirus')
        mc = int(re.search("Positive for 2019-nCoV(.*?)  (.*?)<", rD.text, re.DOTALL).group(2))
    except:
        mc=0

    try:
        rD = requests.get('https://finance.yahoo.com/quote/%5EDJI?p=%5EDJI')
        dow = float(re.search("D\(b\)\" data-reactid=\"14\">(.*?)<", rD.text).group(1).replace(",",""))
    except:
        dow=0

    try:
        rD = requests.get('https://finance.yahoo.com/quote/%5EGSPC?p=%5EGSPC')
        sp = float(re.search("D\(b\)\" data-reactid=\"14\">(.*?)<", rD.text).group(1).replace(",",""))
    except:
        sp = 0

    try:
        rD = requests.get('https://finance.yahoo.com/quote/CLX?p=CLX')
        clx = float(re.search("D\(b\)\" data-reactid=\"14\">(.*?)<", rD.text).group(1).replace(",",""))
    except:
        clx=0
    
    buf = "Wilson's Coronameter "
    buf += ("W %d|%d|%d" % (wc, wd, wr)).ljust(21)
    buf += ("USA %d|%d|%d" % (ac, ad, ar)).ljust(21)
    buf += ("MI %d|%d|%d" % (mc, md, mr)).ljust(21)
    buf += ("DJ %.1f S&P %.1f" % (dow, sp)).ljust(21)
    buf += ("CLX %.1f" % (clx)).ljust(21)
    buf += (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")).ljust(21)

    sock.sendto(buf.encode(), ('192.168.1.44',4212))
    print(buf)

    j = datetime.datetime.now().strftime("%M")
    while j == datetime.datetime.now().strftime("%M"):
        time.sleep(1)