Two Small Projects + Patagonia!

packs I left on February 18th for a month and a half trip to Argentina and Chile to drink, eat, climb, and backpack. This is a quick writeup of two side projects I worked on for that trip.

I may do a full writeup of both of these projects at a later date. Stay tuned for some climbing and backpacking photos from Patagonia!

Over the Door Hangboard

As part of my rock climbing program, I use a hangboard to train my finger strength. One of the biggest challenges with hangboarding in a small NYC apartment is figuring out how to mount it. My current apartment doesn't have enough space above any of the doors to mount a hangboard, nor does it have wooden joists to mount one to.

A few ready-made products exist to mount a hangboard in a doorway, but they are expensive and not very customizable. So, I decided to go the DIY route and mount a hangboard to an over the door pull up bar. Total cost was around $50, not including the hangboard itself.

IMG_1517

The hangboard is mounted to a pine board, which has two 1" pipe flanges attached to it. The end pipes of the pull up bar fit into the flanges.

IMG_1515

I drilled holes through the pipe ends and flanges using a tungsten dremel bit, and bolted them together. The edges were finished with JB Weld for good measure.

IMG_1516

The board flexes slightly under load, but easily supports my 160 lbs. After of month of semi-daily usage I feel very comfortable with it. However, if I were 20+ lbs heavier I would probably reinforce it by connecting the top of the board to the pipe cross brace with steel plumbers tape at a later date, if necessary.

 

Solar Panel USB Charger

One common problem people have in the backcountry these days is keeping all of their various devices charged. I generally carry at least two devices that require a USB charger (headlamp and iPhone), and many people carry more (handheld GPS, smartwatch, etc.).

Like the hangboard mount, there are several existing products on the market for USB solar charging. But, nothing really exists in the cheap (under $30), lightweight (under 10oz) sweet spot I was looking for. So, I went the DIY route with this as well. Total cost $22, total weight 6oz, with a peak power output of 5 volts at 1 amp.

IMG_1509

The very simple circuit consists of two solar panels wired in parallel with a diode, connected to a cheap step-up converter. The step-up converter accepts an input of 2.5-5v and outputs a constant 5v @ up to 2A.

IMG_1513

The LED glows green when it is receiving enough electricity to charge a device over USB, red when the input voltage is too high, and is off when the input voltage is too low.

Green!

IMG_1514

Eventually, I plan to glue fabric to the back of the solar panels with a pouch for power bank that is being charged. For the time being, I am hanging it as-is from the back of my backpack while out trekking.

solar_panels

SmartStarter, part 2

IMG_1526 This is part 2 of a 3 part series on the SmartStarter project.

I overhauled the hardware and software for v2 of the SmartStarter, resulting in a simpler architecture and cleaner code base. Additionally, the SmartStarter has been hardwired into the always on 12v line in my car.

See the full changes below the break!

Changes since v1.0

  • Simplified architecture -- moved to Pro Mini + breakout board mounted on PCB from Uno + breakout board on breadboard
  • Housed in project box -- added hardwired on/off + status LED
  • Cleaned up code -- removed unnecessary clutter in setup and door functions, added status detection code for v3
  • Integrated into 12v always on line in car -- DC / DC buck converter cleans "dirty" power input

Project Goals:

  1. Remotely start or lock/unlock car via text (part 1)
  2. Power device via car always on 12V rail (part 2)
  3. Obtain GPS signal via OEM antenna and push coordinates into database at 30 min intervals (part 3)
  4. Write front end Google Maps overlay for GPS coordinates (part 3)

Hardware Revision

IMG_1483

One of the most substantial changes between v1 and v2 of the SmartStarter is the move from an Arduino Uno + breadboard to an Arduino Pro Mini mounted to a PCB.

While getting parts for another project, I ordered some cheap Chinese clones of the 3.3v Pro Mini (like $1.30 cheap) off of Ebay. Somewhat surprisingly, all 5 of them worked perfectly out of the box--no missing bootloaders, cold solder joints, etc.

I mounted one of the new Pro Minis to a small PCB along with the FONA shield/breakout board.

IMG_1484

In retrospect, I wish I had thought more about spacing when I soldered the pin headers to the PCB. As a result, I was unable to connect the Arduino to the shield with solder traces, and instead opted to create some custom female to female jumpers. I am planning to revise this at a later date.

IMG_1486

IMG_1487

I bought a cheap project box to house the project, and picked up a AA enclosure + batteries for testing purposes. After testing out a few layout configurations, I ended up going with a setup similar to below.

IMG_1489

FullSizeRender

Car Integration

Another major change in v2 is the integration of the project directly into the car's power supply.

Many people are aware that their cars have 12v lines running to the electronic components inside. However, I think most people aren't aware of how noisy those 12v lines actually are. For example, my "12v" line reads ~12.5v when the car is off, ~8v when it is cranking, and ~14.5v while the car is running and the alternator is charging the battery.

This poses a slight challenge when attempting to safely connect electronics components directly, especially something like an Arduino that takes 12v in MAX. One simple and cheap way to get around this is by using a DC/DC buck converter (aka a step down converter). Without getting too technical, these converters efficiently (>90%) step down voltage while stepping up current.

In this case, I opted for a buck converter that took 6-22v in and outputted a clean 5v, drawing 12mA max at up to 95% efficiency (claimed). This covers the low I measured on the line (8v) and any potential small spikes above the high I measured on the line (14.6v).

I hardwired a small switch between the buck converter and the rest of the small circuit, and added a status LED. The LED power is currently pulled from the 3.3v vcc pin on the Arduino, although I plan to add an additional LED pulled from the FONA for v3.

IMG_1494

I tested the circuit out at 14.6v before installing into the car.

IMG_1496

When installing the AV system in my car a few years ago, I ran an always on 12v line to the Pioneer XM adapter I housed under my seat. For this project I tapped the power and ground running to that box into an SAE adapter.

IMG_1525

I wired the other end of the SAE adapter to the buck converter lines running out of the project box. This is the current setup, all housed under the drivers side seat of my car.

IMG_1527

Testing it out!

IMG_1528

I still need to fix the blank space in the response texts (it currently uses the full SMS 160 character limit).

Z(ero) SNES

blue_side Nothing fires up that childhood nostalgia for me quite like the SNES. Some of my favorite childhood memories are of exploring the hidden worlds in Super Mario World, thrashing sewer baddies in Teenage Mutant Ninja Turtles, and catching on fire in NBA Jam.

To recapture some of that magic, I hacked a Raspberry Pi powered emulation device inside of an SNES controller. Running off a Li-Po battery, the controller connects to the TV via an HDMI cable and allows the player to chose from a variety of NES, SNES, and Genesis games. An RGB LED connected to the charging circuit provides status indication for the device: on (blue), low battery (red), charing (yellow), charged (green). The device is named Z(ero) SNES as a nod to the first SNES emulator I ever used, dos-based ZSNES.

Project Goal/Constraints:

  • Build all-in-one emulation device into OEM SNES controller. Device should be self-contained and aesthetically pleasing--smooth(ish) cuts, polished finishes. Device should provide status indication via LED.

Materials Used

  • Official SNES controller
  • Raspberry Pi Zero
  • 16GB MicroSD card
  • Adafruit Power Boost 500c
  • 500mAh Li-Po battery
  • Mini HDMI to HDMI adapter
  • On/off switch
  • RGB LED

Hardware

IMG_1421

Controller Breakdown

IMG_1369

The first and easiest step is breaking the SNES controller. There are five phillips head screws on the back that can easily be removed with a jewler's screwdriver or PC screwdriver.

Next, remove the SNES cable by de-soldering the pin headers from the front section of the board. I highly recommend an all-in-one iron + desoldering pump for this, since heating the pads with an iron and using desoldering braid is a bit of a pain.

IMG_1370

IMG_1371

At this point, we are ready to replace the pins with wires that will connect to the GPIO ports of the Pi. I purposely used the same color wires as the OEM harness to avoid future confusion.

NOTE: the photo below shows the wires soldered into the PCB from front to back. After making a second one of these, I would recommend going back to front to make everything fit into the case better.

IMG_1422

Raspberry Pi

rpi_zero_pinout

I used this handy graphic put together by Anthony Caccese to wire up the Pi itself. Basically, we're hooking the SNES controller up to 3.3v power (white), GPIO3 (red), GPIO17 data (brown), GPIO10 (yellow), and GPIO11 (orange).

The additional red and black wires are the 5v / ground for the charging circuit.

IMG_1423

IMG_1424

led_test2

resistor wiring

fitting

SNES_overhead

yellow_side

Software

One of the most challenging parts of this project turned out to be the initial software configuration. For whatever reason, the version of RetroPie I downloaded from their website and wrote to my SD card had botched kernel headers that would not allow me to install the controller drivers.

In case anyone comes up against that issue in the future, I've created a ready to go image with RetroPie installed and the drivers enabled (current as of 12/2016). There are no ROMs included in the image, you will need to add them over SSH/WiFi.

Backend Setup (via headless SSH)

One of the greatest things I learned while working on this project was that the Pi Zero will allow headless SSH pretty much out of the box. This means that you can use a single USB cable to power/connect to the Pi and modify config files after making a small change to two boot files. See the steps here, big thanks to Andrew Mulholland for his work on this.

After following the steps outlined on Andrew's blog, connect the pi to your computer via the micro USB port labeled USB (not "PWR IN"). Fire up your favorite SSH program (Putty on Windows, terminal on Linux/OSX) and you should be able to connect to the Pi as "pi@retropie.local" password: "raspberry".

Now you can enable the drivers for the controller by running the RetroPi startup script in /xxx/xxx/ :

./path/to/script

Follow menu options 01 xxx > 05 xxx > blah blah

Finally, you will need to update the gamecon driver configuration file in /etc/modprobe.d :

sudo nano /etc/modeprobe.d/gamecon.conf // change line to read "options gamecon_gpio_rpi map=0,0,0,0,0,1"

Press ctrl + x to save and exit.

At this point, you can transfer ROMs to the /xxx/rom/ directory. I personally prefer to use a GUI for doing this, such as WinSCP (Windows) or Cyberduck (OSX).

If you are interested in running updates or installing additional packages, you will need to configure the pi to connect over Ethernet/WiFi and plug in an adapter. You can do this by editing the WPA Supplicant configuration file found at /etc/network/wpa_supplicant.conf

sudo nano /etc/network/wpa_supplicant.conf

Add your wireless network info into a new line of the config file in the following form (replacing network_name and pasword), press ctrl + x to save and exit:

network={ ssid="network_name" psk="password" }

Frontend Setup

Plug in your finished product to the TV and boot it up! It will take 30 seconds to a minute to load into EmulationStation.

Use your keyboard to navigate to the input config menu by pressing Enter > Configure Inputs. Follow the directions to configure the SNES controller buttons.

 

SmartStarter, part 1

car_starter_square This is part 1 of a 3 part series on the SmartStarter project.

  • Part 1 covers hardware, setup, and software development of the Arduino + GSM functionality.
  • Part 2 covers hardware and software revisions and integration of the unit into the car
  • Part 3 covers setup and development of the GPS tracking functionality.

Recently, I've been toying around with the idea of building more complicated IoT devices. Inspired by recent visits to HackManhattan and FatCat Fab Lab, I decided to pull the trigger on an Adafruit GSM shield. Around the same time, I was looking for a way to expand the range of the remote starter in my car--I often end up parked 3-4 blocks away from my apartment, which renders the remote starter in my car useless. Nothing like walking to your car in 20 degree weather and spending another ten minutes in the car freezing your ass off becuase the range on your remote starter is garbage.

And so, out of these two things, the SmartStarter project was born. By combining an Arduino, GSM shield, and a spare car remote, I can now remotely start and lock/unlock my car anywhere I have cell service.

At a later date, I plan to capture GPS coordinates via the onboard GPS chip at 30 minute intervals and push them to a MySQL database on my AWS server.

 

Full breakdown and code below the break.

Project Goals:

  1. Remotely start or lock/unlock car via text (part 1)
  2. Power device via car always on 12V rail (part 2)
  3. Obtain GPS signal via OEM antenna and push coordinates into database at 30 min intervals (part 3)
  4. Write front end Google Maps overlay for GPS coordinates (part 3)

Materials Used

  • Arduino Uno
  • Adafruit FONA 3g GSM shield w/ 3g SIM
  • GSM antenna
  • GPS antenna -- I am splicing into the factory SiriusXM antenna in my car
  • Li-Po battery
  • Spare car remote

Hardware

Any microprocessor programmable via the Arduino IDE will work, in this case I went with an official Arduino Uno. When looking at GSM shields, I was only interested in shields with 3g compatible chipsets. I take my car out to some remote areas when I go climbing and backpacking, so I wanted to guarantee the most amount of coverage possible. I also had some concern with sunsetting 2g coverage rendering my device useless in the near future.

I chose the Adafruit 3g FONA Shield for a few reasons: it's cost effective, it has an onboard GPS chip, the FONA library is well supported in the maker scene, and Adafruit is an awesome NYC based company.

Setup

Arduino + FONA + Antennas + Battery

adafruit_fona

One of the awesome things about Adafruit is that they provide in-depth setup documentation for most of their products. In this case, they have a overview that walks you through basic setup of the FONA 3g GSM shield. For the purposes of this blog post, I'm going to skip those steps, since Adafruit already covers them so well.

Car Remote

I disassembled the car remote to take a look at the buttons used for each item: lock, unlock, and remote start.

[IMG]

I then used a multimeter in continuity setting to get an understanding of how the press button switches worked. I learned that the buttons had two negative (ground) and two positive leads, connected to a 3v coin battery on the other side.  I realized that I could ground the negative side of the switches and use the Arduino to switch between I/O pin mode to effectively tri-state the switches, in order to get an electronic button press.

I soldered connections to each of the necessary pads:

img_1339_text

I then connected the positive leads to the Arduino IO pins and the negative leads to ground on the breadboard:

img_1341_text

Button Press Test

To test my connections, I wrote a basic Arduino program that sent a low signal to pin 5 in output mode, waited a second, and changed the pin to input mode--"pressing" the lock button on the remote.

void setup() {
   pinMode(5, OUTPUT);
}

void loop() {

  int lock(){
    digitalWrite(5, LOW);
    delay(1000);
    pinMode(5, INPUT);
  }  

}

Hooray, it works!

[GIF]

 

Code

At this point, I was ready to write an Arduino sketch. I needed code that would check the SIM card for an SMS, parse the SMS text, and run a specific function based on the SMS text.

Fortunately, Adafruit had a project guide for an SMS controlled door lock that I was able to use as the basis for my SMS read-in code. It already had the bones for the SMS read-in functionality, I just needed to build my specific functionality on top.

Note: all credit for the open-sesame code goes to LadyAda of Adafruit; her full code for that project is on the Adafruit Github page.

My Code

Using the open-sesame code as a base, I wrote a series of functions (doors, remote, and sms) that interact with the key-fob pins depending on the text of the SMS message. After attempting to lock/unlock or remote start the car, the device will send a response SMS back to the sender, indicating that it has attempted to "press" that button.

Success!

[GIF]

Full code on Github [fac_icon icon="github"]

#include 
#include "Adafruit_FONA.h"

#define FONA_RX 2
#define FONA_TX 3
#define FONA_RST 4
#define FONA_RI 5

#define LOCK_PIN 6
#define UNLOCK_PIN 7
#define REMOTE_PIN 8

#define LED 13

#define LOCK true
#define UNLOCK false

#define BUSYWAIT 5000  // milliseconds

// stores senderient SMS #
char sender[25];

// this is a large buffer for replies
char replybuffer[255];

SoftwareSerial fonaSS = SoftwareSerial(FONA_TX, FONA_RX);

Adafruit_FONA_3G fona = Adafruit_FONA_3G(FONA_RST);

uint8_t readline(char *buff, uint8_t maxbuff, uint16_t timeout = 0);

boolean fonainit(void) {
  while (!Serial);

  fonaSS.begin(4800);
  Serial.println(F("Initializing....(May take 3 seconds)"));

  if (! fona.begin(fonaSS)) {
    Serial.println(F("Couldn't find FONA"));
    return false;
  }
  Serial.println(F("FONA is OK"));
  return true;

}

void setup() {

  // set LED output for debugging
  pinMode(LED, OUTPUT);

  // set key fob pins so they are off
  pinMode(LOCK_PIN, INPUT);
  pinMode(UNLOCK_PIN, INPUT);  
  pinMode(REMOTE_PIN, INPUT);

  Serial.begin(115200);
  Serial.println(F("FONA basic test"));

  while (! fonainit()) {
    delay(5000);
  }

  // Print SIM card IMEI number.
  char imei[15] = {0}; // MUST use a 16 character buffer for IMEI!
  uint8_t imeiLen = fona.getIMEI(imei);
  if (imeiLen > 0) {
    Serial.print("SIM card IMEI: "); Serial.println(imei);
  }

  pinMode(FONA_RI, INPUT);
  digitalWrite(FONA_RI, HIGH); // turn on pullup on RI
  // turn on RI pin change on incoming SMS!
  fona.sendCheckReply(F("AT+CFGRI=1"), F("OK"));
}

int8_t lastsmsnum = 0;

void loop() {
   digitalWrite(LED, HIGH);
   delay(100);
   digitalWrite(LED, LOW);

  while (fona.getNetworkStatus() != 1) {
    Serial.println("Waiting for cell connection");
    delay(6000);
  }

  // Check if the interrupt pin went low, 
  // and after BUSYWAIT milliseconds break out to check
  // manually for SMS' and connection status
  for (uint16_t i=0; i<BUSYWAIT; i++) {
     if (! digitalRead(FONA_RI)) {
        // RI pin went low, SMS received?
        Serial.println(F("RI went low"));
        break;
     } 
     delay(1);
  }

  int8_t smsnum = fona.getNumSMS();
  if (smsnum < 0) {
    Serial.println(F("Could not read # SMS"));
    return;
  } else {
    Serial.print(smsnum); Serial.println(F(" SMS on SIM card!"));
  }

  if (smsnum == 0) return;

  // there's an SMS!
  uint8_t n = 0; 
  while (true) {
     uint16_t smslen;
     //char sender[25];

     uint8_t len = fona.readSMS(n, replybuffer, 250, &smslen); // pass in buffer and max len!
     // if the length is zero, its a special case where the index number is higher
     // so increase the max we'll look at!
     if (len == 0) {
        Serial.println(F("[empty slot]"));
        n++;
        continue;
     }
     if (! fona.getSMSSender(n, sender, sizeof(sender))) {
       // failed to get the sender?
       sender[0] = 0;
     }

     Serial.print(F("***** SMS #")); Serial.print(n);
     Serial.print(" ("); Serial.print(len); Serial.println(F(") bytes *****"));
     Serial.println(replybuffer);
     Serial.print(F("From: ")); Serial.println(sender);
     Serial.println(F("*****"));

     if (strcasecmp(replybuffer, "lock") == 0) {
       // lock the doors
       digitalWrite(LED, HIGH);
       doors(LOCK);
       digitalWrite(LED, LOW);
     }
     if (strcasecmp(replybuffer, "unlock") == 0) {
       // unlock the doors
       digitalWrite(LED, HIGH);
       doors(UNLOCK);
       digitalWrite(LED, LOW);
     }
     if (strcasecmp(replybuffer, "remote") == 0) {
       // start/stop the car
       digitalWrite(LED, HIGH);
       remote();
       digitalWrite(LED, LOW);
     }

     delay(3000);
     break;
  }  
  fona.deleteSMS(n);

  delay(1000); 
}

void doors(boolean locked){
  if (locked) {
    pinMode(LOCK_PIN, OUTPUT);
    digitalWrite(LOCK_PIN, LOW);
    delay(1000);
    pinMode(LOCK_PIN, INPUT);

    Serial.println("Attemped to lock doors");
    Serial.print("Attempting to send SMS to: "); Serial.println(sender);
    sms(sender, "Door lock signal sent!");
  } else {
    pinMode(UNLOCK_PIN, OUTPUT);
    digitalWrite(UNLOCK_PIN, LOW);
    delay(1000);
    pinMode(UNLOCK_PIN, INPUT);

    Serial.println("Attemped to unlock doors");
    Serial.print("Attempting to send SMS to: "); Serial.println(sender);
    sms(sender, "Door unlock signal sent!");
  }
}

void remote(){
  pinMode(REMOTE_PIN, OUTPUT);
  digitalWrite(REMOTE_PIN, LOW);
  delay(1000);
  pinMode(REMOTE_PIN, INPUT);

  Serial.println("Attemped to remote start/stop car");
  Serial.print("Attempting to send SMS to: "); Serial.println(sender);
  sms(sender, "Remote start/stop signal sent!");
}

void sms(char* sender, char* message){
  flushSerial();
  if (!fona.sendSMS(sender, message)) {
    Serial.println(F("Failed"));
  } else {
    Serial.print(F("Sent SMS response to: ")); Serial.println(sender);
  }
}

void flushSerial() {
  while (Serial.available())
    Serial.read();
}

NYC Subway Status Touchscreen

img_1202 If you rely on public transportation for your daily commute, you know how frustrating it can be to miss a train. Worse still, if the trains happen to be delayed (or running express past your stop), you may end up walking 20+ minutes to a different stop/line.

This is often the case for me in Brooklyn, where the J and M lines run express past my stop for seemingly no reason. After getting fed up with walking 2 extra stops to catch an express train for the millionth time, I decided to do something about it. I could check Google Maps transit layer on my phone every day, but I honestly never think to pull it up. Instead, I decided to

It turns out that the New York MTA hosts the service status of all train lines and buses in a publicly accessible XML text file on their website. With a little bit of Python scripting, I was able to scrape, clean, and store the subway data. I pushed this data into a Python GUI that allows the user to see the status of each line, and click on the status message to get more details. I loaded the program onto a Raspberry Pi connected to a 7" touchscreen and mounted it by my front door.

See the GUI in action here:

python_gui

Full breakdown and code below the break.

Materials Used

  • Raspberry Pi 2
  • SDHC card with Raspbian
  • Belkin Wifi Adapter (not needed if using Pi 3)
  • Official Raspberry Pi 7" Touchscreen
  • Python 3.5

Hardware

Raspberry Pi 2

I happened to have an old Pi 2 laying around from an XBMC media center build I stopped using a couple years ago. I formatted the SD card with a fresh copy of Raspian and added a wifi adapater to connect Pi to my home wLAN.

Official Raspberry Pi Screen

My goal was to keep this build as simple as possible, so I went with the official Pi 7" touchscreen as the display for this project.

img_1157

The screen itself it pretty much plug and play: display goes via DPI port to the pi, 2 pins to the pi, and 2 pins to the GPIO.

7intscontents

img_1159

5 minutes later and voila, touchscreen pi!

img_1160

At this point, the hardware is good to go. Time to write some code.

Code

Scraping the MTA Data

I decided to use Python for this project because it comes preinstalled on the Pi, has support for the Pi's GPIO pins, and has great web scraping libraries. I personally prefer and use Python 3.x over 2.x, but the same could be accomplished in 2.x with a bit of tweaking.

Below is the code for the basic scraping and parsing functionality:

from bs4 import BeautifulSoup
from urllib.request import urlopen
from collections import OrderedDict

url = 'http://web.mta.info/status/serviceStatus.txt'
xml = urlopen(url).read()
soup = BeautifulSoup(xml, "xml")

timestamp = soup.timestamp.text
subway = soup.subway

status_dict = {}

all_lines = subway.findAll('line')
for line in all_lines:
    line.find('line')
    for info in line:
        name = line.find('name').text
        status = line.find('status').text
        text = line.find('text').text
        text = text.replace('<','<').replace('>','>').replace(' ',' ')

        if line.find('Date').text == '': 
            datetime = timestamp
        else:
            datetime = line.find('Date').text.strip(' ') + ' ' + line.find('Time').text.strip(' ')
        status_line = [status, datetime, text]
    status_dict[name] = status_line

sorted = OrderedDict(sorted(status_dict.items()))
print(sorted)

In the code above, we start by importing the relevant libraries: BeautifulSoup, URLOpen, and OrderedDict.

from bs4 import BeautifulSoup
from urllib.request import urlopen
from collections import OrderedDict

Next, we setup BeautifulSoup to read in the XML file by providing the URL for the file, opening it, and parsing it with the XML library.

url = 'http://web.mta.info/status/serviceStatus.txt'
xml = urlopen(url).read()
soup = BeautifulSoup(xml, "xml")

At this point, we only care about two XML tags: timestamp and subway. We don't want bus times, and we will need the timestamp tag later. We should also create a dictionary to store all of the data.

timestamp = soup.timestamp.text
subway = soup.subway

status_dict = {}

Within the subway tag, there exists individual lines, which contain tags for the line name, line status, and description of that status. We can write a simple nested loop that pulls out each line from subway, and all 3 tags from each line.

all_lines = subway.findAll('line')
for line in all_lines:
    line.find('line')
    for info in line:
        name = line.find('name').text
        status = line.find('status').text
        text = line.find('text').text

At this point, we have the data we want and can do some basic cleanup on it. For whatever reason, the MTA doesn't update the individual line time stamp for lines where service status == "GOOD SERVICE". In order to provide a complete data, we need to use the time stamp we got earlier from the parent XML file. This is slightly complicated by the fact that the time stamp in the XML file is in a different format from the time stamp provided by the line status. So, we need to first check if the line time stamp exists, use the parent time stamp if not, or use the line time stamp and reformat it.

        text = text.replace('<','<').replace('>','>').replace(' ',' ')

        if line.find('Date').text == '': 
            datetime = timestamp
        else:
            datetime = line.find('Date').text.strip(' ') + ' ' + line.find('Time').text.strip(' ')

Finally, we take each of these tags and stick them into a list item. This list item goes into a dictionary, which gets sorted and put into an ordered dictionary. This way, the lines will always be returned in the same order. This example then goes on print the dictionary; see the full code below to see how this is integrated into the GUI.

        status_line = [status, datetime, text]
    status_dict[name] = status_line

sorted = OrderedDict(sorted(status_dict.items()))
print(sorted)

 

Python GUI

I used Python library Tkinter to build the GUI for this project, which was probably the hardest part of the whole thing. I had 0 experience with Tkinter (or UI design in general) when I started. That said, I think it turned out well.

Full code available on GitHub [fac_icon icon="github"]

###   Created by Nick Wallace  ###
###                            ###
###                            ###

import re
from tkinter import *
from tkinter import font
from bs4 import BeautifulSoup
from urllib.request import urlopen
from PIL import ImageTk, Image
from collections import OrderedDict

root = Tk()
root.title("MTA Service Status")
root.configure(background = "white")
#root.geometry('800x480')
#root.attributes('-fullscreen', True)

root.columnconfigure(0, weight = 1)
root.columnconfigure(1, weight = 1)
root.columnconfigure(2, weight = 1)
root.columnconfigure(3, weight = 1)

header_font = font.Font(family='Heveltica', weight = 'bold', size=13)
main_font = font.Font(family = 'Heveltica', size = 12)

def getData():
    global sort

    url = 'http://web.mta.info/status/serviceStatus.txt'
    xml = urlopen(url).read()
    soup = BeautifulSoup(xml, "xml")
    timestamp = soup.timestamp.text
    subway = soup.subway
    status_dict = {}
    all_lines = subway.findAll('line')
    for line in all_lines:
        line.find('line')
        for info in line: 
            name = line.find('name').text
            status = line.find('status').text
            text = line.find('text').text
            text = text.replace('<','<').replace('>','>').replace(' ',' ')

            if line.find('Date').text == '': 
                datetime = re.sub(':[:]*.{2}[:]* {1}', '', timestamp)
            else:
                datetime = line.find('Date').text.strip(' ') + ' ' + line.find('Time').text.strip(' ')
            status_line = [status, datetime, text]
        status_dict[name] = status_line
    sort = OrderedDict(sorted(status_dict.items()))
    return sort

def firstRun(dict):
    global labels
    labels = {}

    header_img = Label(root, text = 'LINE', font = header_font, bg = 'white')
    header_img.grid(columnspan = 2)

    header_name = Label(root, text = 'STATUS', anchor = 'center', font = header_font, bg = 'white')
    header_name.grid(row = 0, column = 2)

    header_timestamp = Label(root, text = 'TIME', anchor = 'center', font = header_font, bg = 'white')
    header_timestamp.grid(row = 0, column = 3)

    rc = 1

    for k, v in dict.items():
        img_url = 'c://Temp/imgs/' + k + '.png'
        img = ImageTk.PhotoImage(Image.open(img_url))

        Grid.rowconfigure(root, rc, weight=1)
        Grid.columnconfigure(root, rc, weight=1)

        line_img = Label(root, image = img, bg = 'white')
        line_img.image = img
        line_img.grid(row = rc, columnspan = 2)

        #line_name = Label(root, text = k, bg = 'white')
        #line_name.grid(row = rc, column = 1, sticky = W)
        labels[(rc, 1)] = k

        line_status = Label(root, text = ' ' + v[0], font = main_font, bg = 'green' if v[0] == 'GOOD SERVICE' else 'yellow' if v[0] == 'PLANNED WORK' else 'red')
        line_status.grid(row = rc, column = 2)
        if v[0] not in ['GOOD SERVICE']: line_status.bind('', addMessage)
        labels[(rc, 2)] = line_status

        line_timestamp = Label(root, text = ' ' + v[1], font = main_font, bg = 'white')
        line_timestamp.grid(row = rc, column = 3)
        labels[(rc, 3)] = line_timestamp

        rc += 1

    blank_line = Label(root, text = '', bg = 'white')
    blank_line.grid(row = rc)

def addMessage(event): 
    global msg_label

    grid_info = event.widget.grid_info()
    line = labels[(grid_info['row'],1)]

    msg_text = re.sub('<[^>]*>', '', sort[line][2])
    msg_text = re.sub('&[^;]*;', ' ', msg_text)
    msg_text = re.sub('\n+', '\n', msg_text)
    msg_text = re.sub(' +', ' ', msg_text)
    msg_text = re.sub('Show.*?Note:', '', msg_text)
    msg_text = re.sub('Key.*?Note:', '', msg_text)
    msg_text = re.sub(r' [ad].*relay.', '', msg_text)

    msg_label = Label(root, text = msg_text, anchor = 'w', justify = 'center', wraplength=480)
    msg_label.grid(row = grid_info['row'] + 1, columnspan = 4)
    msg_label.bind('', removeMessage)

def removeMessage(event):
    msg_label.grid_forget()   

def refresh(dict):
    rc = 1

    for k, v in dict.items():
        labels[(rc,2)].config(text = ' ' + v[0], bg = 'green' if v[0] == 'GOOD SERVICE' else 'yellow' if v[0] == 'PLANNED WORK' else 'red')
        labels[(rc,3)].config(text = ' ' + v[1])

        rc += 1

def exit():
    root.quit()

firstRun(getData())

refreshButton = Button(root, text = "Refresh", command =lambda: refresh(getData()), height = 1, width = 15).grid(row = 15, column = 0, columnspan = 3) 
exitButton = Button(root, text = "Exit", command = exit, height = 1, width = 15).grid(row = 15, column = 2, columnspan = 3)
blank_line = Label(root, text = '', bg = 'white')
blank_line.grid(row = 16)

mainloop()

 

GUI in Action

The GUI is still a WIP, but click below to see the basic functionality: python_gui