HTMAA 2024
Sungmoon Lim

week 9


output devices

This week's assignment is to add an output device to a microcontroller board I've designed, and program it to do something.

Because I anticipated needing some sort of output device for this week, I soldered an OLED display to my microcontroller board last week. I will be using this same board, but writing a different program for a different type of output.

Again, here is my PCB schematic:

schematicweek9

And here is my PCB design:

designweek9

And here is my milled and soldered board. For output week, I'll focus on the switch and the OLED.

pcb9.3

Although my final project doesn't involve an OLED screen, this week has a lot of relevance to my final project; in that spirit, I decided to play around with the MTA API.

Here is my code for the NQRW lines for 14th-St Union Station. The OLED will display the next 4 northbound departures on the NQRW lines at the Union Square Station:

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <EEPROM.h>
#include <time.h>

// Display settings
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

// Configuration structure
struct Config {
  char stopId[8];
  uint8_t i2cBus;
  uint8_t leastMinutesAhead;
  bool debugMode;
} config;

// Global variables
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WiFiManager wifiManager;
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000;  // EST: UTC-5
const int daylightOffset_sec = 3600;  // 1 hour DST

// API endpoint for N/Q/R/W lines
const String MTA_BASE_URL = "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-nqrw";

void loadConfig() {
  EEPROM.begin(512);
  EEPROM.get(0, config);
  EEPROM.end();
  
  // Set defaults 
  if (config.stopId[0] == 0xFF) {
    strcpy(config.stopId, "R16N");  // 14th Street-Union Square station ID for N/Q/R/W
    config.i2cBus = 1;
    config.leastMinutesAhead = 0;
    config.debugMode = false;
    saveConfig();
  }
}

void saveConfig() {
  EEPROM.begin(512);
  EEPROM.put(0, config);
  EEPROM.commit();
  EEPROM.end();
}

void setupDisplay() {
  Wire.begin(21, 22);  // SDA, SCL pins
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
}

void clearDisplay() {
  display.clearDisplay();
  display.display();
}

String getTimeString(time_t timestamp) {
  struct tm timeinfo;
  localtime_r(×tamp, &timeinfo);
  char timeStr[6];
  strftime(timeStr, sizeof(timeStr), "%H:%M", &timeinfo);
  return String(timeStr);
}

void displayTransitTimes() {
  if (WiFi.status() != WL_CONNECTED) {
    if (config.debugMode) Serial.println("WiFi not connected");
    return;
  }

  HTTPClient http;
  http.begin(MTA_BASE_URL);
  
  int httpCode = http.GET();
  
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    DynamicJsonDocument doc(32768);  // Increased buffer size for GTFS feed
    
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      if (config.debugMode) {
        Serial.print("JSON parsing failed: ");
        Serial.println(error.c_str());
      }
      return;
    }

    if (config.debugMode) {
      Serial.println("\nUpcoming arrivals at 14th Street-Union Square:");
    } else {
      display.clearDisplay();
      display.setCursor(0,0);
    }

    // Parse GTFS feed for 14th Street-Union Square station arrivals
    JsonArray trips = doc["entity"];
    
    int displayedCount = 0;
    time_t now;
    time(&now);

    for (JsonVariant trip : trips) {
      if (trip["trip_update"]["stop_time_update"]) {
        for (JsonVariant stopTime : trip["trip_update"]["stop_time_update"].as<JsonArray>()) {
          if (strcmp(stopTime["stop_id"], config.stopId) == 0) {
            time_t arrivalTime = stopTime["arrival"]["time"].as<time_t>();
            int minutesAhead = (arrivalTime - now) / 60;
            
            if (minutesAhead >= config.leastMinutesAhead) {
              String timeStr = getTimeString(arrivalTime);
              String routeId = trip["trip_update"]["trip"]["route_id"].as<String>();
              
              if (config.debugMode) {
                Serial.print(routeId);
                Serial.print(": ");
                Serial.print(timeStr);
                Serial.print(" (");
                Serial.print(minutesAhead);
                Serial.println(" min)");
              } else {
                display.print(routeId);
                display.print(": ");
                display.print(timeStr);
                display.print(" (");
                display.print(minutesAhead);
                display.println("m)");
              }
              
              displayedCount++;
              if (displayedCount >= 4) break;  // Show max 4 arrivals
            }
          }
        }
      }
    }
    
    if (!config.debugMode) {
      display.display();
    }
  } else {
    if (config.debugMode) {
      Serial.print("HTTP request failed with code: ");
      Serial.println(httpCode);
    }
  }
  
  http.end();
}

void setup() {
  Serial.begin(115200);
  
  // Load saved configuration
  loadConfig();
  
  // Setup display
  setupDisplay();
  
  // Configure WiFiManager
  wifiManager.autoConnect("MTA_Display_Setup");
  
  // Configure time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  
  if (config.debugMode) {
    Serial.println("Setup complete");
  }
}

void loop() {
  displayTransitTimes();
  delay(30000);  // Update every 30 seconds
}

But... it didn't work.

I am actually fairly confident in the code, but Arduino IDE kept giving me the below error:

designweek9

Apparently it wasn't able to establish a connection with the microcontroller.

Which is especially strange since this is the same board as I used for input week, and the board worked just a day ago with no issues. The XIAO's light was on and I confirmed that things were working as intended via the multimeter. I ran through all of the standard troubleshooting procedures and rebooted the XIAO, but nothing changed.

I ended up going to office hours for help but Anthony wasn't able to troubleshoot it, either; after trying it with a different computer system (his), we got a different alert that specified that it was a hardware error, not a software error. Anthony suspected that because I had the PCB in my backpack where it made consistent, sustained contact with my MacBook, that might have affected the microcontroller (since Macs are alluminum). Because all of the joints and soldering remained unchanged and the board was routed correctly (and the OLED worked) as evidenced last week, I decided to replace the ESP32 with a new one and try re-soldering it.

Which, of course, in the procress of doing so, the OLED broke off from the board...

Because I already proved that the output device (OLED) worked for this board last week and it meets the output week requirements, I decided to cut my losses and table re-creating this board for now. Instead, I will shift focus on finalizing the board for my final project, which includes LEDs as its main output device.