Coronameter
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.
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)