Soren Gilkey Johnson
Published © GPL3+

ESP32 GTFS-Based Bus Tracker for the M15 NYC Bus Line

I built an open-source, modular ESP32-based bus tracker that lets users track buses using GTFS data, using a 3D and spare components I had.

IntermediateFull instructions provided2 hours55
ESP32 GTFS-Based Bus Tracker for the M15 NYC Bus Line

Things used in this project

Hardware components

3 mm LED: Green
3 mm LED: Green
×21
Espressif ESP32 Development Board - Developer Edition
Espressif ESP32 Development Board - Developer Edition
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
×1
Resistor 100 ohm
Resistor 100 ohm
×21

Software apps and online services

VS Code
Microsoft VS Code
ESP-IDF
Espressif ESP-IDF

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)
(Optional)

Story

Read more

Custom parts and enclosures

New York M15 Bus Map

This is the base of the project, and it is what every other component connects to.

Schematics

M15 Tracker Schematic

Map of routing from ESP32 dev board to LED's and buttons.

Code

M15 Tracker Main Code

C/C++
This is the main code in the project which connects to WIFI, gets the current bus location, parses the file, and updates the LED's.
#include <string>
#include <vector>
#include <cmath>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_http_client.h"
#include "driver/gpio.h"

#include "gtfs-realtime.pb.h"

#define WIFI_SSID "" // Enter your WIFI network's name
#define WIFI_PASS "" // Enter your WIFI network's password
#define API_KEY "" // Enter your API key (only if service requires it)
#define GTFS_URL "" API_KEY // This is the URL that downloads the position of the buses (ex. https://gtfsrt.prod.obanyc.com/vehiclePositions?key=)
#define ROUTE_ID "MTA NYCT_M15"

static const char* TAG = "GTFS";

// ----- GPIOs -----
#define BUTTON_GPIO GPIO_NUM_0  // Example button GPIO, change it to your real IO pin.
#define DEBOUNCE_MS 200 // Avoid mechanical bounce errors 

// ----- Bus stop struct -----
struct BusStop {
    const char* name;
    double lat;
    double lon;
    gpio_num_t pin;
};

// ----- M15 major stops with GPIOs -----
// You can replace these with other bus stop locations, as well as other GPIO pins
BusStop m15_stops[] = {
    {"South Ferry/Terminal", 40.7010, -74.0130, GPIO_NUM_2},
    {"Water St/Pine St", 40.7065, -74.0080, GPIO_NUM_4},
    {"Pearl St/Beekman St", 40.7120, -74.0000, GPIO_NUM_5},
    {"Madison St/Catherine St", 40.7150, -73.9970, GPIO_NUM_12},
    {"Allen St/Grand St", 40.7135, -73.9940, GPIO_NUM_13},
    {"1 Av/E 1 St", 40.7280, -73.9830, GPIO_NUM_14},
    {"1 Av/E 15 St", 40.7320, -73.9800, GPIO_NUM_15},
    {"1 Av/E 25 St", 40.7370, -73.9800, GPIO_NUM_16},
    {"1 Av/E 29 St", 40.7390, -73.9790, GPIO_NUM_17},
    {"1 Av/E 34 St", 40.7445, -73.9780, GPIO_NUM_18},
    {"1 Av/E 43 St", 40.7500, -73.9750, GPIO_NUM_19},
    {"1 Av/Mitchell Pl", 40.7510, -73.9740, GPIO_NUM_21},
    {"1 Av/E 57 St", 40.7600, -73.9670, GPIO_NUM_22},
    {"1 Av/E 67 St", 40.7645, -73.9610, GPIO_NUM_23},
    {"1 Av/E 81 St", 40.7780, -73.9550, GPIO_NUM_25},
    {"1 Av/E 86 St", 40.7805, -73.9535, GPIO_NUM_26},
    {"1 Av/E 97 St", 40.7880, -73.9490, GPIO_NUM_27},
    {"1 Av/E 106 St", 40.7940, -73.9450, GPIO_NUM_32},
    {"1 Av/E 116 St", 40.7975, -73.9400, GPIO_NUM_33}
};

// ----- Haversine -----
// Used to determine which bus stop is closest to a bus.
double haversine(double lat1, double lon1, double lat2, double lon2) {
    const double R = 6371000;
    double dLat = (lat2-lat1) * M_PI/180;
    double dLon = (lon2-lon1) * M_PI/180;
    double a = sin(dLat/2)*sin(dLat/2) +
               cos(lat1*M_PI/180)*cos(lat2*M_PI/180) *
               sin(dLon/2)*sin(dLon/2);
    return 2*R*atan2(sqrt(a), sqrt(1-a));
}

// ----- Find closest stop -----
// Loop through all stops to find the closest
BusStop find_closest_stop(double lat, double lon) {
    double min_dist = 1e9;
    BusStop closest = m15_stops[0];
    for (auto &stop : m15_stops) {
        double d = haversine(lat, lon, stop.lat, stop.lon);
        if (d < min_dist) { min_dist=d; closest=stop; }
    }
    return closest;
}

// ----- Wi-Fi init -----
void wifi_init() {
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    wifi_config_t wifi_config = {};
    strncpy((char*)wifi_config.sta.ssid, WIFI_SSID, sizeof(wifi_config.sta.ssid));
    strncpy((char*)wifi_config.sta.password, WIFI_PASS, sizeof(wifi_config.sta.password));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_LOGI(TAG, "Connecting to Wi-Fi...");
    ESP_ERROR_CHECK(esp_wifi_connect());
    vTaskDelay(pdMS_TO_TICKS(5000)); // Wait 5 seconds
}

// ----- GPIO init -----
// If all LED's are inverted change the 0 to a 1
void gpio_init() {
    for (auto &stop: m15_stops) {
        gpio_pad_select_gpio(stop.pin);
        gpio_set_direction(stop.pin, GPIO_MODE_OUTPUT);
        gpio_set_level(stop.pin, 0);
    }
    gpio_pad_select_gpio(BUTTON_GPIO);
    gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
    gpio_pullup_en(BUTTON_GPIO);
}

// ----- Bus info -----
struct BusInfo {
    std::string id;
    double lat;
    double lon;
    BusStop closest_stop;
};
std::vector<BusInfo> buses;
int selected_bus_index = 0;

// ----- Fetch GTFS ----- 
void fetch_gtfs_rt() {
    //Performs HTTP GET to fetch GTFS-realtime vehicle positions
    esp_http_client_config_t config = {};
    config.url = GTFS_URL;
    config.timeout_ms = 5000; // 5 second timeout
    esp_http_client_handle_t client = esp_http_client_init(&config);

    if (esp_http_client_perform(client) == ESP_OK) {
        int content_length = esp_http_client_get_content_length(client);
        if (content_length <= 0) { esp_http_client_cleanup(client); return; }
        std::vector<uint8_t> buffer(content_length);
        esp_http_client_read(client, reinterpret_cast<char*>(buffer.data()), content_length);
        //Parses the Protobuf feed
        TransitRealtime::FeedMessage feed;
        if (feed.ParseFromArray(buffer.data(), content_length)) {
            buses.clear();
            for (int i=0;i<feed.entity_size();i++) {
                const auto &entity = feed.entity(i);
                if (entity.has_vehicle()) {
                    const auto &vehicle = entity.vehicle();
                    if (vehicle.has_trip() && vehicle.trip().route_id() == ROUTE_ID) {
                        BusInfo b;
                        b.id = vehicle.vehicle().id();
                        b.lat = vehicle.position().latitude();
                        b.lon = vehicle.position().longitude();
                        b.closest_stop = find_closest_stop(b.lat, b.lon);
                        buses.push_back(b);
                    }
                }
            }
        }
    }
    esp_http_client_cleanup(client);
}

// ----- Update GPIOs -----
// Change the 0 and 1 if having inverted-LED problems
void update_bus_gpio() {
    // Clear all pins first
    for (auto &stop: m15_stops) gpio_set_level(stop.pin,0);
    if (!buses.empty()) {
        BusInfo &b = buses[selected_bus_index];
        gpio_set_level(b.closest_stop.pin,1);
        ESP_LOGI(TAG,"Selected Bus: %s -> %s (GPIO %d)", b.id.c_str(),
                 b.closest_stop.name, b.closest_stop.pin);
    }
}

// ----- Button Task -----
// Switches to next bus when button is pressed
void button_task(void *pv) {
    int last_state = 1;
    while(true) {
        int state = gpio_get_level(BUTTON_GPIO);
        if (state==0 && last_state==1) { // Falling edge
            selected_bus_index++;
            if (selected_bus_index >= buses.size()) selected_bus_index=0;
            update_bus_gpio();
            vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
        }
        last_state = state;
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

// ----- GTFS Task -----
// Fetches GFTS data every 15 seconds
void gtfs_task(void *pv) {
    while(true) {
        fetch_gtfs_rt();
        update_bus_gpio(); // update selected bus after fetch
        vTaskDelay(pdMS_TO_TICKS(15000));
    }
}

// ----- Main -----
extern "C" void app_main() {
    wifi_init();
    gpio_init();
    xTaskCreate(gtfs_task,"gtfs_task",8192,NULL,5,NULL);
    xTaskCreate(button_task,"button_task",4096,NULL,5,NULL);
}

Credits

Soren Gilkey Johnson
1 project • 1 follower
I am a student in electrical engineering, who has always been interested in building cool things!

Comments