本文展示如何为物联网设备设计和建模数据的示例。在这里使用M5Stack(这是一种带有显示屏的小型模块化物联网设备)并连接到纽约市大都会交通管理局(NYCMTA)的API,以呈现各个地铁车站的列车实时到站时间。

译者 | 李睿

审校 | 孙淑娟

近年来,物联网设备成为全球关注的热点。物联网设备的应用日益广泛,从显示天气情况的闹钟到列出食品价格的冰箱。无论具体情况如何,这些设备都依赖API与数据源进行通信。但是,究竟如何连接消息、数据和设备呢? 

本文展示如何为物联网设备设计和建模数据的示例。在这里使用M5Stack(这是一种带有显示屏的小型模块化物联网设备)并连接到纽约市大都会交通管理局(NYCMTA)的API,以呈现各个地铁车站的列车实时到站时间。

虽然专注于M5Stack,但将讨论的概念将适用于跨各种设备设计物联网应用程序。

一、先决条件

本文将专注于有关如何从API请求数据的更大的概念性想法。一些编程知识将会非常有用。虽然不需要M5Stack,但如果有M5Stack,那么可以继续并将完成的项目上传到自己的设备上。

考虑到这一点,可以下载VS Code IDE和M5Stack插件。如果以前从未启动过M5Stack,那么按照他们的指南设置WiFi和必要的固件。对于这个项目将使用Python3,它是M5Stack使用的主要编程语言。

为此需要注册一个纽约大都会运输署(NYC MTA)开发者帐户以获得免费的开发者API密钥,以访问他们的实时地铁数据。

最后,应该注册一个免费的Gravitee帐户以使用API设计器,这将使其更轻松地可视化和理解API调用中的数据流。

这个项目的原始材料受到这个开源项目的启发,所以如果有所帮助的话,那么继续启动该存储库。

二、设计API交互

在编写一行代码之前,要考虑需要什么样的信息来完成这个项目:

  • 地铁站的相关信息。

  • 哪些列车经过这些地铁站。

  • 这些列车的最新实时数据。

根据文档,API分为静态数据馈送和实时数据馈送。

静态数据馈送包含有关站点的信息。有了这些信息,就可以从实时数据馈送API中获取实际的实时地铁数据。纽约大都会运输署(MTA)提供的数据采用以下CSV格式:

Spreadsheet
stop_id,stop_code,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station1.2.

由于需要的唯一静态信息是站点ID,因此可以简单地随机抽取一个站点ID并将其用于实时提要。在这种情况下,选择Hoyt-Schermerhorn站是因为它相对复杂:两列单独的地铁车通过该站(A和C)。而车站还通过北行(N)还是南行(S)来识别。

Spreadsheet
A42,,Hoyt-Schermerhorn Sts,,40.688484,-73.985001,,,1,A42N,,Hoyt-Schermerhorn Sts,,40.688484,-73.985001,,,0,A42
A42S,,Hoyt-Schermerhorn Sts,,40.688484,-73.985001,,,0,A421.2.3.4.

从这些行中,只需要父站ID(A42)来识别通过车站的地铁,包括北行(A42N)和南行(A42S)。 

实时提要以谷歌的GTFS格式表示,该格式基于协议缓冲区(也称为protobuf)。虽然纽约大都会运输署(NYC MTA)没有记录其特定提要的示例,但GTFS可以记录。从GTFS文档中,可以确定如何以protobuf格式获取最新列车在特定车站的到达时间。

以下是来自GTFS端点的响应示例,已转换为JSON以便于可视化:

JSON                   
{                      "trip":{               
 "trip_id":"120700_A..N",
 "start_time":"20:07:00","start_date":"20220531",
 "route_id":"A"        },                     "stop_time_update":[   {                      
  "arrival":{          
  "time":1654042672    
  },                   
  "departure":{        
  "time":1654042672    
  },                   
  "stop_id":"H06N"     
  },                   
                       
 //…more stops…      
                       
 {                     
 "arrival":{           
 "time":1654044957     
 },                    
   "departure":{       
  "time":1654044957    
   },                  
  "stop_id":"A42N"     
  }                    
 ]                     
 }                     1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.

由于NYC MTA API提供了大量的信息,使用Gravitee API设计器来模拟API返回的内容,映射和可视化数据是非常有用的。以下是API设计器思维导图:

图片

API设计器可以帮助识别API的所有资源(端点),以及与资源关联的数据属性。这些属性将包括端点需要的输入和它提供的输出。 

在这个地图中,有一个路径为/gtfs/的资源。可以根据需要附加更多的属性,并且可以用数据类型注释每个属性。通过查看这个地图,可以绘制一条从端点到右下角确定的到达和离开时间的直接路径。 

因此,为了表示需要的数据,需要: 

  • 确定想要从中获取地铁信息的车站的ID。

  • 针对感兴趣的地铁线路向NYC MTA的GTFS提要发出HTTP请求。

  • 迭代结果,将响应数组中的stop_id与站点ID进行比较。

  • 然后可以根据特定车站和地铁的时间信息采取行动。

这代表了一些活动部件,但它不应该是无法处理的事情。

三、对其进行编码

在M5Stack上运行任何程序之前,首先确保代码在本地工作。在此将安装一些Python软件包,使项目更容易构建。

Shell
pip3 install --upgrade gtfs-realtime-bindingspip3 install protobuf3_to_dict
pip3 install requests1.2.3.4.

前两个包将协议缓冲区转换为Python字典(或散列),这使得数据模型更易于使用。最后一个包使从Python发出HTTP请求变得更加容易。 

以下将通过导入Python包来启动程序: 

Pythonfrom google.transit import gtfs_realtime_pb2
import requests
import time1.2.3.4.

接下来,将向NYC MTA GTFS提要发出HTTP请求: 

Python
api_key = "YOUR_API_KEY"

 headers = {'x-api-key': api_key}feed = gtfs_realtime_pb2.FeedMessage()response = requests.get('https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-ace',headers=headers)feed.ParseFromString(response.content)1.2.3.4.5.6.7.8.9.10.

到目前为止,一切顺利。在这里使用的GTFS端点是用于A/C/E列车的端点,可以通过URL上的-ace后缀来识别它。(除了这个演示,在此不关注列车E。) 

以下将GTFS协议缓冲区响应转换为字典: 

Pythonfrom protobuf_to_dict import protobuf_to_dict
subway_feed = protobuf_to_dict(feed) realtime_data = subway_feed['entity']1.2.3.4.

在这一点上,强烈建议发出print(realtime_data),这样就可以看到实际的数据结构是什么样的。如果这是一个真实的项目,这样的分析可能会帮助确定字典中的哪些键和值需要迭代。 

Python
 def station_time_lookup(train_data, station):for trains in train_data:
 if trains.__contains__('trip_update'):
 unique_train_schedule = trains['trip_update']
 if unique_train_schedule.__contains__('stop_time_update'):
 unique_arrival_times = unique_train_schedule['stop_time_update']
 for scheduled_arrivals in unique_arrival_times:
 stop_id = scheduled_arrivals.get('stop_id', False)
 if stop_id == f'{station}N':
 time_data = scheduled_arrivals['arrival']
 unique_time = time_data['time']
 if unique_time != None:northbound_times.append(unique_time)
 elif stop_id == f'{station}S':
 time_data = scheduled_arrivals['arrival']
 unique_time = time_data['time']
 if unique_time != None:southbound_times.append(unique_time)

 # Keep a global list to collect various train times
northbound_times = []
 southbound_times = []

 # Run the above function for the station ID for Hoyt-Schermerhorn
 station_time_lookup(realtime_data, 'A42')1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.

为此突然有很多代码!但别担心,正在做的事情并没有那么复杂: 

为此遍历A/C线路的地铁信息数组。 

对于每个数组条目,验证是否拥有需要的所有键的值。这是防御性编码,因为不能百分之百确定这个第三方服务在需要的时候有需要的事物。 

之后,遍历所有车站信息,并在登陆需要的北行和南行列车的父ID(A42)时停止。 

最后,在两个单独的全局变量中保留即将到来的列车到达时间列表。

接下来,展示这些信息:

Python
# Sort collected times in chronological ordernorthbound_times.sort()southbound_times.sort()# Pop off the earliest and second earliest arrival times from the list
nearest_northbound_arrival_time = northbound_times[0]second_northbound_arrival_time = northbound_times[1]nearest_southbound_arrival_time = southbound_times[0]
 second_southbound_arrival_time = southbound_times[1]

 ### UI FOR M5STACK SHOULD GO HERE ###

 def print_train_arrivals(
 direction,
 time_until_train,
 nearest_arrival_time,
 second_arrival_time):
 if time_until_train <= 0:
 next_arrival_time = second_arrival_time
 else nearest_arrival_time:
 next_arrival_time_s = time.strftime(
 "%I:%M %p",
 time.localtime(next_arrival_time))
 print(f"The next {direction} train will arrive at {next_arrival_time_s}")

 # Grab the current time so that you can find out the minutes to arrival
 current_time = int(time.time())
 time_until_northbound_train = int(
 ((nearest_northbound_arrival_time - current_time) / 60))
 time_until_southbound_train = int(
 ((nearest_southbound_arrival_time - current_time) / 60))
 current_time_s = time.strftime("%I:%M %p")
 print(f"It's currently {current_time_s}")

 print_train_arrivals(
 "northbound",
 time_until_northbound_train,
 nearest_northbound_arrival_time,
 second_northbound_arrival_time)
 print_train_arrivals(
 "southbound",
 time_until_southbound_train,
 nearest_southbound_arrival_time,
 time_until_southbound_train)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.

上面所做的大部分工作都是数据格式化。关键步骤如下: 

  • 对北行和南行列车在车站的到达时间进行排序。

  • 乘坐前两次(“最快”的地铁到达)。

  • 将这些时间与当前时间进行比较,以获得地铁到达的距离(以分钟为单位)。将这些地铁到达时间传递给print_train_arrivals。

  • 如果下一班地铁不到一分钟就到了,将会显示第二次到站时间。恐怕赶不上那班地铁!否则,将显示最近的到达时间。

如果在终端上运行此脚本,则应看到以下类似消息:

Shell
It's currently 05:59 PMThe next northbound train will arrive at 06:00 PMThe next southbound train will arrive at 06:02 PM1.2.3.4.

四、部署到M5Stack

现在已经在本地测试了Python代码可以与NYC MTA API通信,是让这个代码在M5Stack上运行的时候了。对M5Stack进行编程的最简单方法是通过免费的UI Flow IDE,它只是一个通过WiFi与设备通信的网页。可以通过他们的文档了解有关如何配置设备以进行WiFi访问的更多信息。 

虽然M5Stack可以通过所见即所得的用户界面(WYSIWYG UI)元素进行编程,但它也可以接受(并运行)Python代码。然而,所见即所得元素的主要优点是它使在屏幕上绘制的文本更容易可视化:

图片

在这个GIF中,在示例M5Stack屏幕上创建了一个带有默认字符串“Text”的标签。当切换到Python时,看到标签是一个名为M5TextBox的对象的实例化。当标签被拖动时,三维X和Y坐标(构造函数中的前两个参数)在Python中会发生变化。这样可以很容易地看到程序将如何显示。还可以通过单击标签本身来更改Python代码中使用的变量(以及其他属性): 

图片

大多数情况下,编写的Python脚本只需稍作修改即可在M5Stack上使用。可以从本地机器复制Python代码并将其粘贴到UI Flow IDE的Python选项卡中。 

在代码中,找到### UI FOR M5STACK SHOULD GO HERE ###注释并将其下方的所有内容替换为以下代码: 

Python
 time_label = M5TextBox(146, 27, "", lcd.FONT_Default, 0xFFFFFF, rotate=0)
 northbound_label = M5TextBox(146, 95, "", lcd.FONT_Default, 0xFFFFFF, rotate=0)
 southbound_label = M5TextBox(146, 163, "", lcd.FONT_Default, 0xFFFFFF, rotate=0)

 def print_train_arrivals(direction,label,time_until_train,
 nearest_arrival_time,
 second_arrival_time):
 if time_until_train <= 0:
 next_arrival_time = second_arrival_time
 else nearest_arrival_time:
  next_arrival_time_s = time.strftime(
  "%I:%M %p",
  time.localtime(next_arrival_time))
 label.setText(f"The next {direction} train will arrive at {next_arrival_time_s}")

 while True:
 # Grab the current time so that you can find out the minutes to arrival
 current_time = int(time.time())
 time_until_northbound_train = int(
  ((nearest_northbound_arrival_time - current_time) / 60))
  time_until_southbound_train = int(
 ((nearest_southbound_arrival_time - current_time) / 60))
 current_time_s = time.strftime("%I:%M %p")
 time_label.setText(f"It's currently {current_time_s}")

 print_train_arrivals(
 "northbound",
  northbound_label,
 time_until_northbound_train,
  nearest_northbound_arrival_time,
  second_northbound_arrival_time)
 print_train_arrivals(
 "southbound",
 southbound_label,
 time_until_southbound_train,
 nearest_southbound_arrival_time,
 time_until_southbound_train)

 sleep 51.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.

其中大部分应该看起来很熟悉,有两个主要修改可以让这个代码在M5Stack上运行。 

首先,创建了作为时间和地铁数据占位符的标签: 

  • 时间标签(time_label)

  • 北向标签(northbound_label)

  • 南向标签(southbound_label)

其次,将所有内容都放在了一个while循环中,它将获取当前时间并设置标签文本。循环将休眠五秒钟,然后重新启动该过程。 

就是这样!当点击运行(Run)时,应该看到地铁字符串每五秒更新一次,并使用最新的路线数据。 

五、结论

物联网设备经常被爱好者使用,但如果继续从事这个项目,在现实世界中有几个的考虑因素。一个考虑因素是速率限制,确保以有效的方式从MTA API请求数据。另一个考虑因素是连接性。如果设备暂时无法访问WiFi,它将如何重新建立连接以获取所需的信息? 

一旦开始考虑这些生产级问题,或者如果想在多个设备上扩展其项目,还需要考虑API管理。以上提到Gravitee Designer,这在设计阶段非常有用。Gravitee还有其他用于API管理的工具,例如API网关、监控和实时分析、部署。 

对于习惯于为传统服务器和Web浏览器编写代码的开发人员来说,物联网应用程序开发似乎令人生畏。然而,物联网设备的功能提升实际上很小。如今的设备内置了对流行语言和框架的支持,使物联网成为一种有趣且创新的方式来构建或集成API和应用程序。

原文链接:https://dzone.com/articles/building-an-iot-application-using-an-http-api-1

责任编辑:武晓燕来源: 51CTO技术栈