×

Ultra96 FPGA上的Live NYC Subway Monitor应用程序

消耗积分:0 | 格式:zip | 大小:3.74 MB | 2022-12-14

370217

分享资料个

描述

在隔离期间我发现自己在家的额外时间里,我最近在他们的开发者门户上发现了谷歌的交通 API。这些 API 提供了实时跟踪美国大城市公共交通的挂钩。许多主要城市的交通系统都利用了这一点,并以谷歌的通用交通信息规范 (GTFS) 格式提供火车、公共汽车等的实时跟踪信息。

GTFS 是一种用于公共交通时刻表和相关地理信息的开放数据格式。GTFS 有一个名为 GTFS Realtime 的扩展,它是一个供公共交通机构向应用程序开发人员提供有关其车队的实时更新的提要。纽约市大都会交通管理局 (MTA) 为其每条地铁线路创建了 GTFS 实时提要,这些提要用于地铁站的实时倒计时显示,直到下一趟火车,也用于 GPS 应用程序(如谷歌地图)以帮助计算用户在公共交通工具上请求两个位置之间的路线时的总行程时间。MTA 大约每 30 秒用新数据更新这些提要。

作为大流行前的纽约人,我早上通勤中最令人沮丧的部分是在我的火车上门即将关闭时走进车站,然后不得不等待 20 多分钟才能到达下一站(在布鲁克林)。作为大流行后的纽约人,一旦我的办公室重新开放并且我不再在家工作,我非常想尽可能地限制我在火车站度过的时间。

由于我的 Ultra96 已经与我的各种其他 SDR Web 应用程序一起在我的家庭网络上建立了自己的永久固定装置,我决定添加另一个 Web 应用程序,我可以在我的手机上调用它来查询 A/C/E 线路的实时馈送和解析出 C 线的特定警报和跟踪信息,这样我就可以在我离开公寓的时间与北行 C 线列车到达我当地车站的时间相匹配。

pYYBAGOYzuKAKxryAAP-xuzANsM998.png
有趣的事实:纽约市的 C 线拥有仍在运行的最古老的火车车厢 (R-32s)。
 

考虑到项目构想,在 Ultra96 FPGA 开发板上开发 Web 应用程序的主要步骤:

  • 创建自定义项目(通常是 Python 脚本)。
  • 创建将自定义项目集成到 Ultra96 的网络服务器中的 Web 应用程序 Python 后端。
  • 创建 Web 应用程序 HTML 前端作为项目的用户界面网页。
poYBAGOYiOqAAX-oAAT4Mn1e2-w584.png
我的 Ultra96 V2 硬件设置与电源线和 JTAG/UART 到 USB pod。
 

要访问地铁线路的这些实时信息,MTA 需要从其开发人员门户生成的 API 密钥。这是为了将任何提要在任何时间点看到的流量保持在合理的水平。为了遵守 MTA 的使用指南,重要的是要注意这个项目在 Ultra96 的网络服务器上而不是在 MTA 的服务器上运行,它绝不是 MTA 许可的,不能保证准确/及时,最重要的是这个项目是纯娱乐。如果您计划使用 MTA 的实时供稿重新创建/扩展此项目,请确保您了解 MTA 的使用规则和指南

在描述我是如何在 Ultra96 上设置我的 Web 应用程序以查询这些实时提要之前的一些背景知识,GTFS Realtime 基于协议缓冲区(proto2) 语法。协议缓冲区语言最初是由Google开发的,作为存储和交换结构化信息的优化方法。它使用接口描述语言(IDL) 来描述给定数据集的结构,然后它实现一个程序,该程序从该描述生成源代码,然后用于生成或解析表示结构化数据的字节流。总的来说,与将数据序列化为 XML 或蟒蛇酸洗

GTFS 实时提要包含三个主要的提要实体(又名数据类型):行程更新、服务警报和车辆位置。这些提要实体可以以任何所需的方式组合以创建自定义提要。MTA 以这种方式创建了自己的提要,以将自己的自定义扩展添加到 GTFS Realtime。提要通过 HTTP 提供,如我之前提到的,每隔 30 秒更新一次,并且由于提要的输出文件是常规的二进制文件,因此任何类型的网络服务器都可以托管和提供该文件。这意味着Ultra96的基于 Flask 的网络服务器可以发送有效的 HTTP GET 请求以返回供在 Ultra96 上处理的提要数据。

New York City Transit Subway 有 12 个提要,用于与地铁列车的运动/状态相关的所有各种数据集:FeedMessage、FeedHeader、NyctFeedHeader、TripReplacementPeriod、TripUpdate、TripDescriptor、NyctTripDescriptor、StopTimeUpdate、NyctStopTimeUpdate、StopTimeEvent、VehiclePosition 和警报。对于这个项目,我选择使用 FeedMessage 提要,因为它是任何其他 NYCT (NYC Transit) 扩展提要(NyctFeedHeader、NyctTripDescriptor 和 NyctStopTimeUpdate)引用的完整数据集。有关 12 个提要中每一个提要的说明,请参阅 MTA 开发人员门户网站上题为“纽约市地铁的 GTFS 实时参考”的附件文档。

Ultra96 上的自定义项目将是一个 Python 脚本,用于查询 A/C/E 地铁线路的 MTA 实时馈送,并在运行时返回用户指定的车站的曼哈顿方向(北行)C 列车的到达时间。相应的自定义网页将获取此到达时间数组并为用户列出它们,以便用户不仅可以看到当前设置下一班火车到达的时间,还可以看到当前在 C 上运行的火车的所有后续到达时间尚未停在指定车站的线路。

当查询 A/C/E 行的实时提要时,它会返回所有三行的数据,直到 Ultra96 上的 Python 脚本对其进行解析,然后仅将所需的数据传递给网页。目前,我已对其进行硬编码,仅查找前往曼哈顿的 C 列车的行程更新实体,因为我住在布鲁克林足够远的地方,以至于我很少从当地车站乘坐布鲁克林/南开的 C 列车。

总体而言,在开始开发 Ultra96 之前,需要从 MTA 的开发人员门户获取两件事:用于 A/C/E 行的实时提要的链接,以及能够查询提要的 API 访问密钥。在此处创建 MTA 开发者帐户并登录后,我在屏幕顶部的“访问密钥”菜单选项下生成了自己的 API 访问密钥,我被要求填写一些基本联系信息以换取密钥(在如果违反使用条款,MTA 将停用密钥)。然后在 Feeds 菜单选项和 Subway Realtime Feeds 子菜单下,我复制了 A/C/E 行的链接。

poYBAGOYzvaAGEyWAAJ0BCNGFR4335.png
选择您感兴趣的火车线路的实时馈送。
 

要开始使用 Ultra96 本身,需要使用 Python 包管理器 pip 安装 Python 的 GTFS 实时绑定模块:

pip3 install --upgrade gtfs-realtime-bindings

虽然用于 HTTP 客户端和发送 HTTP/1.1 请求的 Python 模块已经安装在用于 Web 服务器的 Ultra96 上,但我发现它们需要升级/更新才能使其与 GTFS Realtime 一起使用:

pip3 install --upgrade urllib
pip3 install --upgrade requests
pYYBAGOYiJ-ALwqNAAcA01as54U294.png
Ultra96 上的自定义内容主页。
 

在 Ultra96 的自定义内容网页上,我创建了一个新的自定义项目,它打开 Ultra96 上的文本编辑器来编写 Python 脚本。由主函数调用的核心函数 c_train() 使用我唯一的 API 密钥向 MTA 实时提要发送一个简单的 GET 请求。返回提要后,脚本会解析每个实体,寻找 C 列火车的 route_id 标签。通过检查trip_id 中的“N”标签来进一步过滤这些实体中的每一个,以表明它是北行列车。这些提要实体中的每一个都包含一个 StopTimeUpdate 子实体,其中包含一组数据,这些数据对应于火车尚未停靠的所有车站以及该火车在这些车站的当前估计到达时间。每个站都有一个 stop_id 标签,我在这个网站上找到了整个列表将每个 stop_ids 转换为相应的站名(我为此创建了子函数 stationId_to_stationName())。

就个人而言,我发现与其列出每列火车当前到达的具体时间,倒计时时间列表对我来说更清楚一些。因此,例如,不要显示下一班火车将在下午 2:30 到达,而是显示下一班火车将在 25 分钟后到达。这就是倒计时时钟显示当前在车站本身中的工作方式,所以也许这就是我偏向于这种输出数据格式的原因。该脚本简单地通过使用 datetime Python 模块查询当前时间从时间戳计算倒计时时间,然后从估计的到达时间中减去当前时间,以获得用作倒计时到达时间的时间增量。这些倒计时到达时间的数组被返回给主函数。

地铁跟踪项目Python代码:

import os
import re
import sys
import time
import pytz
import json
import urllib
import requests
import datetime
import subprocess
from pytz import timezone
from google.transit import gtfs_realtime_pb2

def stationId_to_stationName(stationId):
    if (stationId == "A09N"):
        stationName = "168 St"
                    
    elif (stationId == "A10N"):
        stationName = "163 St - Amsterdam Av"
                    
    elif (stationId == "A11N"):
        stationName = "155 St"
                    
    elif (stationId == "A12N"):
        stationName = "145 St"
                    
    elif (stationId == "A14N"):
        stationName = "135 St"
                    
    elif (stationId == "A15N"):
        stationName = "125 St"
                    
    elif (stationId == "A16N"):
        stationName = "116 St"
                    
    elif (stationId == "A17N"):
        stationName = "Cathedral Pkwy - 110 St"
                    
    elif (stationId == "A18N"):
        stationName = "118 St"
                    
    elif (stationId == "A19N"):
        stationName = "96 St"
                    
    elif (stationId == "A20N"):
        stationName = "86 St"
                    
    elif (stationId == "A21N"):
        stationName = "81 St - Museum of Natural History"
                    
    elif (stationId == "A22N"):
        stationName = "72 St"
                    
    elif (stationId == "A24N"):
        stationName = "59 St - Columbus Circle"
                    
    elif (stationId == "A25N"):
        stationName = "50 St"
                    
    elif (stationId == "A27N"):
        stationName = "42 St - Port Authority Bus Terminal"
                    
    elif (stationId == "A28N"):
        stationName = "34 St - Penn Station"
                    
    elif (stationId == "A30N"):
        stationName = "23 St"
                    
    elif (stationId == "A31N"):
        stationName = "14 St"
                    
    elif (stationId == "A32N"):
        stationName = "W 4 St - Wash Sq"
                    
    elif (stationId == "A33N"):
        stationName = "Spring St"
                    
    elif (stationId == "A34N"):
        stationName = "Canal St"
                    
    elif (stationId == "A36N"):
        stationName = "Chambers St"
                
    elif (stationId == "A38N"):
        stationName = "Fulton St"
                
    elif (stationId == "A40N"):
        stationName = "High St"
                
    elif (stationId == "A41N"):
        stationName = "Jay St - MetroTech"
                
    elif (stationId == "A42N"):
        stationName = "Hoyt - Schermerhorn Sts"
                
    elif (stationId == "A43N"):
        stationName = "Lafayette Av"
                
    elif (stationId == "A44N"):
        stationName = "Clinton-Washington Avs"
                
    elif (stationId == "A45N"):
        stationName = "Franklin Av"
                
    elif (stationId == "A46N"):
        stationName = "Nostrand Av"
                
    elif (stationId == "A47N"):  
        stationName = "Kingston - Throop Avs"
                
    elif (stationId == "A48N"):
        stationName = "Utica Av"
                    
    elif (stationId == "A49N"):
        stationName = "Ralph Av"
                    
    elif (stationId == "A50N"):
        stationName = "Rockaway Av"
                    
    elif (stationId == "A51N"):
        stationName = "Broadway Jct"
                    
    elif (stationId == "A52N"):
        stationName = "Liberty Av"
                    
    elif (stationId == "A53N"):
        stationName = "Van Siclen Av"
                    
    elif (stationId == "A54N"):
        stationName = "Shepherd Av"
                    
    elif (stationId == "A55N"):
        stationName = "Euclid Av"

    else:
        stationName = "Invalid C line station ID"

    #print(stationName)
    return stationName

def c_train(StationIdRequested):
    #this just sets a default station in case passed argument is null
    if (StationIdRequested == ""):
        StationIdRequested == "A44N"
    
    headers = {
        "x-api-key": 'YOUR GENERATED API KEY FROM THE MTA HERE'
    }
    
    feed = gtfs_realtime_pb2.FeedMessage()
    response = urllib.request.Request('https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-ace', headers=headers)
    xml = urllib.request.urlopen(response)
    
    feed.ParseFromString(xml.read())
    
    arrival_times = []
    for entity in feed.entity:
        if (entity.trip_update.trip.route_id == "C"):
            stopCntr = 0
            
            dir_str = entity.trip_update.trip.trip_id
            direction = dir_str.find("N")
            
            if (direction != -1):
                for x in entity.trip_update.stop_time_update:
                    StationId = entity.trip_update.stop_time_update[stopCntr].stop_id
                    
                    if (StationId == StationIdRequested):
                        # Train has yet to stop at Clinton-Washington Avs
                        stationArr = entity.trip_update.stop_time_update[stopCntr].arrival
                        stationDpt = entity.trip_update.stop_time_update[stopCntr].departure
                        
                        stationName = stationId_to_stationName(StationId)
                        
                        arrString = str(stationArr)
                        arrNum = re.findall(r'\d+', arrString)
                        arrFloat = float(arrNum[0])
                        
                        dptString = str(stationDpt)
                        dptNum = re.findall(r'\d+', dptString)
                        dptFloat = float(dptNum[0])
                        
                        arrivalTime = datetime.datetime.utcfromtimestamp(arrFloat)
                        departTime = datetime.datetime.utcfromtimestamp(dptFloat)
                        
                        currTime = datetime.datetime.now(datetime.timezone.utc)
                        currTimeNaive = currTime.replace(tzinfo=None)
                        time_delta = arrivalTime - currTimeNaive
                        total_seconds = time_delta.total_seconds()
                        minutes = total_seconds/60

                        #print statement for debugging purposes
                        #print('Manhattan bound C train:', entity.trip_update.trip.trip_id, 'arriving at', stationName,'in', minutes, 'minutes')

                        arrival_times.append(minutes)
                        
                    stopCntr = stopCntr + 1
                    
    return arrival_times
    
def main(arg):
    arrivalTimes = c_train(arg)
    print(arrivalTimes)

if __name__ == "__main__":
    main(sys.argv[1])

返回到自定义内容主网页,我选择了编辑 Webapp 选项,然后为运行项目脚本的自定义网页创建了一个新的后端和前端。需要注意的是,前端和后端文件的文件名必须相同,这样 Ultra96 上的模板才能正确生成新的 webserver Python 脚本。

poYBAGOYiQaAC_EnAAY6-mkwyC4359.png
在 Ultra96 上的 Web 应用程序中创建一个新网页。
 

Python 后端将负责处理前端的请求,并将请求的站 ID 作为参数传递给 Python 脚本的 main 函数。项目脚本拉取并解析C行倒计时到达时间,然后返回到后端,后端将数据传递给前端显示给用户。

poYBAGOYiVKAL3pSAAcvbRrsZvs205.png
未经编辑的后端文件模板。
 

Python后端代码:

@app.route("/c_train_tracking.html", methods=["GET", "POST"])
def c_train_tracking():
    if request.method == "POST":
        station = request.form.get("stations", None)
        if station!=None:
            proc = subprocess.Popen('python3 /usr/share/ultra96-startup-pages/webapp/templates/CustomContent/custom/c_train_tracking.py '+station+'' ,stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
            output,err = proc.communicate()
            
            if(station == "A41N"):
                name = "Jay St - MetroTech"
            elif(station == "A42N"):
                name = "Hoyt - Schermerhorn Sts"
            elif(station == "A43N"):
                name = "Lafayette Av"
            elif(station == "A44N"):
                name = "Clinton - Washington Avs"
            
            return render_template("CustomContent/custom_front_end/c_train_tracking.html", output=output, station=station, name=name)
    return render_template("CustomContent/custom_front_end/c_train_tracking.html")

HTML 前端用作用户界面,下拉菜单允许用户选择所需的车站以查询所有未来的火车倒计时到到达时间(以分钟为单位)(是的,我确实计划编写 C 火车的其余部分就像我在项目 Python 脚本中所做的那样,将来会在此处驻留)。

poYBAGOYiR2AM6mpAAczYHgXQX8397.png
未经编辑的前端文件模板。
 

HTML前端代码:

{% extends "Default/default.html" %}
{% block content %}

<div class="page-header">
  <h1 class="display-4"><b>{% block title %}C Train Tracker{% endblock %}b>h1>
div>


<h1>Station Wait Times for Manhattan-bound C Trainsh1>
<meta http-equiv="explore" content="B" />


<p>Select station from drop down:p>
<form id="form1" action="/c_train_tracking.html" method="POST" enctype="multipart/form-data">
    <select id="stations_dropdown" name="stations">
        <option disabled="disabled" selected="selected" value="A44N">Select Stationoption>
        <option value="A41N">Jay St - MetroTechoption>        
        <option value="A42N">Hoyt - Schermerhorn Stsoption>   
        <option value="A43N">Lafayette Avoption>              
        <option value="A44N">Clinton - Washington Avsoption>  
    select>
    <input type="submit" value="Submit">
form>
<br><br>

<h2>Arrival Times for {{ name }}h2>
<p id="times">p>

<script>
var myObj, i, x = "";
myObj = {
  "arrivalTimes":{{ output }}
};

for (i in myObj.arrivalTimes) {
  x += myObj.arrivalTimes[i] + " minutes
";
}
document.getElementById("times").innerHTML = x;
script>



{% endblock %}

完成前端和后端并返回到 Reload Webapp 页面后,我选中了包含我的新页面的选项,然后单击按钮重新加载 webapp。

一旦 Ultra96 重新启动并连接回我的 Wi-Fi,这是我第一次查询下拉菜单中的一个站的结果:

poYBAGOYz0-AVb5eAAemyMorKWY968.png
 

成功。

当一个项目变得既实用又有趣时,这真是太棒了,我很高兴在客人连接到我的 Wi-Fi 时向他们发送链接(只要生活足够正常,朋友们可以再次访问)当他们准备回家时使用它们来计时从我的公寓步行到当地车站的时间。尤其是在傍晚时分,火车改成更零星的夜间时间表。

 
我现在可能不会离开家,但展示我对这个项目的预期用途很有趣。
 

这也将为我在隔离后的早晨例行工作提供一些额外的结构,因为我将有办法准确衡量我需要多慢或多快才能在车站赶上火车。

poYBAGOYz1-AQcgeAAGJ3YQu-PI757.png
一些 HTML 魔法确保数据在移动设备上易于阅读。
 

我发现的唯一问题是,由于 MTA 大约每 30 秒更新一次提要,因此运行底层项目 Python 脚本来查询提要可能需要大约相同的时间。所以我注意到每隔一段时间,在点击所选电台的提交按钮后,到达时间刷新最多需要 30 秒。

pYYBAGOYz2iARB_oAAXv5Mnv6CM627.png
就像我的咖啡一样,我的 Ultra96 已成为我日常工作中的永久主食。
 

这个项目有很大的扩展空间。最终,我将使硬编码参数也可配置,例如方向(北行与南行)以及查询哪列火车(特别是因为快车 A 火车在深夜接管了当地 C 火车的停靠站)。直到下一次....


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !