物联网设备的VOIP和SMS/MMS解决方案
本文主要介绍用于物联网设备的语音IP(VOIP)和SMS实现。我们将使用Twilio API来编码此解决方案。
- 下载 VOIPService-noexe.zip - 18.1 MB
- 下载 VOIPService.zip - 18.1 MB
- 下载 TwilioClientRaspberryPI.zip - 8.8 MB
引言
注意 - 关于本文的几点说明。不久前,David Hunt提出了PiPhone,一个基于树莓派的GSM手机,这激发了我做很多事情,包括一个完整的VOIP通信。本文并非模仿他的工作,而是要构建一个可用于多种用途的东西,您将在下面看到。
http://techcrunch.com/2014/04/28/the-piphone-is-a-diy-cellphone-powered-by-raspberry-pi/
我们不会重新发明轮子,而是利用合适的服务为物联网设备构建一个支持VOIP和SMS的服务。我将考虑Twilio提供商,但您也可以选择其竞争对手,如Plivo等。它们的工作方式应该相似。
在本文中,我将介绍一个完全使用JavaFX在Windows平台上开发并通过优化以在树莓派上运行的应用程序。底层VOIP通信完全通过利用Twilio API在后台进行。
关于实现Twilio客户端应用程序的需求,这里有一些背景信息。我一直在研究和尝试理解Twilio,以便能够将语音和SMS功能集成到物联网应用程序中。虽然Twilio提供了足够的信息和库供人们使用和构建应用程序,但在没有实际操作经验的情况下,我发现理解它的工作方式有些困难。我承认他们网站上提到的用例很有用且有意义。然而,我花了些时间深入研究他们的文档,才了解到语音通话功能。
Twilio的文档有一些关于可以开发的应用程序的演练,并且有几个使用客户端-服务器端技术(如ASP.NET MVC,PHP和Node JS等)构建的代码示例。然而,没有为物联网设备构建的独立应用程序。
随着物联网开发的快速发展和需求的日益增长,我一直希望能构建能够通过消息、语音等方式与人类进行交互的物联网应用程序。像Twilio这样的公司致力于提供后端解决方案,通过简单地集成Twilio提供的SDK,帮助物联网开发者构建应用程序。
工具和技术
让我们来讨论一下在实现应用程序时将要使用的工具和技术。
该应用程序是使用Java技术设计和开发的。以下是您在树莓派上构建和部署应用程序所需安装的软件。
- NetBeans IDE 8.0 – 一个开源的应用程序开发IDE。您可以从 https://netbeans.org/downloads/ 下载(Java SE应该足够)。
- JavaFX Scene Builder 2.0 – 为应用程序UI设计提供图形用户界面。您可以从 http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html 下载。
- Windows Azure 表存储。
- 托管在Windows Azure上的WebAPI。
背景
以下是一些您可能需要阅读的内容,以了解我在实现Twilio客户端应用程序时将使用的技术。
对于JavaFX教程,我强烈建议您阅读以下链接
http://code.makery.ch/java/javafx-8-tutorial-intro/
http://code.makery.ch/blog/update-to-javafx-8-whats-new/
这是一个有用的实用工具,您可以使用它来查看存储在Azure表存储中的联系人详细信息。
https://azurestorageexplorer.codeplex.com
我建议您阅读以下链接,以了解如何轻松地从NetBeans IDE 8.0远程开发和部署应用程序到树莓派。
https://www.youtube.com/watch?v=ebHbDlTnV-I#t=10
关于Twilio的几点说明
让我们对Twilio有一个基本的了解。Twilio是一家位于加利福尼亚州旧金山的云通信公司。它允许您通过使用其针对各种技术的库,以编程方式拨打语音电话或收发短信(也包括多媒体短信)。
您可以将您的网页变成电话,与朋友通话或订购披萨,或者与物联网设备通信。SMS功能可以轻松构建,以便物联网设备可以发送或接收短信给注册用户。Twilio还有很多功能,我们很快就会看到它的实际实现。
下面是Twilio通信的快照。您使用Twilio的设备或平台无关紧要。由于他们使用的技术,一切都可以立即工作。当涉及到使用WebRTC技术进行拨打或接听电话时,Twilio会利用WebRTC技术。
图片来源 - http://www.getapp.com/blog/twilio-review/
您可能会想,作为开发者或实现解决方案的客户,不必关心它内部如何工作。但我们作为开发者必须对如何使用他们的库有所了解。
在开始之前,您必须在Twilio注册。有一个充值账户以便进行语音/短信通信。Twilio提供一个电话号码,您可以使用它来拨打或接听电话,或者如果您有自己的电话号码,您可以验证您的号码并使用它。但对于短信通信,它不能那样工作。"发件人"号码不能是您的手机号码,所以您需要Twilio的电话号码。有关Twilio与已验证号码的区别的更多信息,请参阅以下链接
在处理来电或短信时,Twilio使用一种称为TwiML的东西,它本质上是一种非常类似于HTML的标记语言,可以在浏览器中渲染。但对于Twilio,TwiML会被渲染到呼叫设备。要更深入地了解TwiML,您可以参考以下链接
https://www.twilio.com/docs/api/2008-08-01/twiml
为什么选择NetBeans IDE?
我之所以选择NetBeans而不是其他IDE,一个主要原因是因为它轻量且易于使用。此外,NetBeans 8.0具有远程部署功能,允许直接将应用程序可执行文件部署到物联网设备。如果您习惯使用Eclipse,也可以选择它。
为什么选择Java技术?
当我在研究如何在运行Linux操作系统的树莓派上使用Azure服务时,我几乎找不到基于Mono C#的Azure库或SDK。我曾考虑使用Mono C#通过Azure REST API实现,这是最后的选择。在开发跨平台、拥有强大库且易于使用的应用程序时,我选择了使用Java技术。这并不意味着我放弃了其他技术,我只是觉得构建一个我选择的编程语言的应用程序。
我必须说一件事;Azure对Java的支持程度很高,微软官方提供了Azure Java库。您也可以使用PHP或Node JS等其他技术。然而,那些不是我的专业领域,我不想冒险。
设置开发环境
现在让我们安装开发所需的必要软件。
从以下链接下载并安装NetBeans IDE 8.0。目前,您可以只选择Java SE选项。
https://netbeans.org/downloads/
下载并安装Oracle Screen Builder。我们需要Screen Builder来设计我们应用程序的用户界面。
http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html
我们将树莓派用作物联网设备。以下链接有助于使用Raspbian Wheezy镜像设置树莓派。
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/RaspberryPiFX/raspberryfx.html
使用代码
现在让我们通过代码示例,让您真实地感受如何轻松地将语音和短信通信集成到您的应用程序中。
我们的Twilio客户端应用程序是一个基于JavaFX的应用程序,完全使用Java技术和Twilio Java库开发。
如果您是JavaFX新手,您可以阅读背景部分提到的文章,以便轻松掌握代码。
这是应用程序主屏幕的快照。您可以在下方看到,屏幕允许您输入电话号码并进行呼叫或挂断。此外,您可以搜索/添加联系人,发送短信等。
高层来看,该应用程序包含三个包。默认情况下,应用程序遵循MVC(模型-视图-控制器)架构。
- TwilioClientRaspberryPI – 这个包包含了大部分逻辑,涵盖了应用程序的大部分功能。
- TwilioClientRaspberryPI.Graphics – 这个包包含了我们在应用程序中使用的所有图形(PNG图片)。
- TwilioClientRaspberryPI.Config – 这个包包含config.properties文件,它是一个应用程序级别的配置设置,用于保存各种信息,例如Twilio和Azure账户的密钥信息。我们还有用于读取属性文件并返回包含应用程序配置设置的实体(entity)的帮助类。
读取应用程序配置
这是我们应用程序的应用程序配置设置。
# Application configuration settings ACCOUNT_SID = <account_sid> AUTH_TOKEN = <account_token> VerifiedPhoneNumber = <verified_phone_number> SMSFromNumber = <sms_from_number> AccountName = <account_name> AccountKey = <account_key> VoiceURL = <voice_url>
让我们尝试理解读取config.properties应用程序配置文件机制。
以下是代码片段,包含读取config文件作为输入流的逻辑,然后使用Properties类加载它,并遍历它,将所有配置信息收集到一个PropertyEntity中,以便我们可以返回它。
public class PropertyFileHelper {
public PropertyEntity Read() {
PropertyEntity propertyEntity = new PropertyEntity();
Properties prop = new Properties();
InputStream inputStream = null;
try {
String filename = "/twilioclientraspberrypi/config/Config.properties";
inputStream = getClass().getResourceAsStream(filename);
if (inputStream == null) {
return propertyEntity;
}
prop.load(inputStream);
Enumeration<?> e = prop.propertyNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
switch(key){
case "ACCOUNT_SID":
propertyEntity.setAccount_sid(prop.getProperty(key));
break;
case "AUTH_TOKEN":
propertyEntity.setAuth_token(prop.getProperty(key));
break;
case "AccountName":
propertyEntity.setAccount_name(prop.getProperty(key));
break;
case "AccountKey":
propertyEntity.setAccount_key(prop.getProperty(key));
break;
case "VerifiedPhoneNumber":
propertyEntity.setVerified_phone(prop.getProperty(key));
break;
case "VoiceURL":
propertyEntity.setVoice_url(prop.getProperty(key));
break;
case "SMSFromNumber":
propertyEntity.setSmsfrom_number(prop.getProperty(key));
break;
default: break;
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
return propertyEntity;
}
}
拨打和挂断电话
让我们通过代码来了解如何轻松实现拨打和挂断电话。
以下是相应的代码片段。
我们将处理呼叫和挂断按钮的点击事件并编写一些逻辑。在我们深入之前,有几点需要了解。那就是,只有当我们实现并设置了voice URL时,呼叫才会启动。我们设置的URL参数提供了拨打电话的指令。这就是TwiML发挥作用的地方。当我们尝试通过调用CallFactory的create方法来发起呼叫,并传入To、From和URL参数时,它将进一步发出一个Web请求,传入源和目标查询字符串参数。
我们将很快看到负责启动呼叫等的WebAPI实现。注意 - 无论是Web服务还是其他什么,重要的是我们设置的URL必须提供TwiML,以便Twilio解析器可以解析它并采取必要的行动。
请注意,CallFactory实例是基于Account实例创建的。Account实例是基于TwilioRestClient实例创建的;这需要有效的Twilio Account SID和Token值。
通过对Call实例的hungup方法进行单行调用即可挂断电话。
client = new TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN); mainAccount = client.getAccount(); btnCall.setGraphic(new ImageView(CallImg)); btnCall.setOnAction((ActionEvent e) -> { final CallFactory callFactory = mainAccount.getCallFactory(); final Map<String, String> callParams = new HashMap<String, String>(); String number = String.format("%s%s", "+", txtPhoneNumber.getText()); String voiceUrl = String.format("%s?source=%s&target=%s", propertyEntity.getVoice_url(), VerifiedPhoneNumber, number); callParams.put("To", number); callParams.put("From", VerifiedPhoneNumber); callParams.put("Url", voiceUrl); try { call = callFactory.create(callParams); } catch (TwilioRestException ex) { Logger.getLogger(MainScreenController.class.getName()).log(Level.SEVERE, null, ex); } }); btnEndCall.setGraphic(new ImageView(DeclineImg)); btnEndCall.setOnAction((ActionEvent e) -> { if(call != null) try { call.hangup(); } catch (TwilioRestException ex) { Logger.getLogger(MainScreenController.class.getName()).log(Level.SEVERE, null, ex); } });
添加联系人信息
这是应用程序中添加联系人的屏幕快照。
现在是时候看看如何添加联系人信息,以便以后可以搜索和选择电话号码进行呼叫或查看联系人信息等。
为了访问Azure表存储,我们需要构建一个包含协议、账户名和密钥的连接字符串,以便应用程序能够连接到表存储并对其执行CRUD操作。
storageConnectionString = "DefaultEndpointsProtocol=https;" + "AccountName=" + propertyEntity.getAccount_name() + ";" + "AccountKey=" + propertyEntity.getAccount_key();
这里我们使用Windows Azure表存储来管理联系人详细信息。首先要确保创建一个表,如果不存在的话。
- 创建云存储账户实例。
- 基于存储账户实例获取云表客户端实例。
- 创建新的云表实例,指定表名和表客户端。
- 调用createIfNotExists方法,如果不存在则会创建一个新的云存储表。
private void CreateTableIfNotExist() { try { CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString); // Create the table client. CloudTableClient tableClient = storageAccount.createCloudTableClient(); // Create the table if it doesn't exist. CloudTable cloudTable = new CloudTable(tableName,tableClient); cloudTable.createIfNotExists(); } catch (Exception e) { // Output the stack trace. e.printStackTrace(); } }
以下是添加联系人信息的代码片段。我们这样做。
- 创建云存储账户实例。
- 基于存储账户实例获取云表客户端实例。
- 基于指定的表名创建新的云表实例。
- 创建新的ContactEntity实例,指定“FirstName”、“LastName”,然后设置电子邮件地址和电话号码。
- 获取TableOperation实例以插入或替换联系人信息。
- 最后,调用CloudTable实例的execute方法来插入或替换联系人详细信息。
private void Add(){ try { CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString); // Create the table client. CloudTableClient tableClient = storageAccount.createCloudTableClient(); // Create a cloud table object for the table. CloudTable cloudTable = tableClient.getTableReference(tableName); // Create a new contact entity. ContactEntity contact = new ContactEntity(txtFirstName.getText().toUpperCase(), txtLastName.getText().toUpperCase()); contact.setEmail(txtEmail.getText()); contact.setPhoneNumber(txtPhoneNumber.getText()); TableOperation insertContact = TableOperation.insertOrReplace(contact); // Submit the operation to the table service. cloudTable.execute(insertContact); SetObservableCollection(contact); lblInfo.setText("Saved Sucessfully!"); } catch (Exception e) { // Output the stack trace. e.printStackTrace(); lblInfo.setText(e.getMessage()); } }
获取所有联系人信息
现在让我们通过代码来获取存储在Azure存储中的所有联系人。以下是相应的代码片段。
它的工作原理如下。
- 创建云存储账户实例。
- 基于存储账户实例获取云表客户端实例。
- 基于指定的表名创建新的云表实例。
- 接下来,我们将构建一个不应用任何过滤条件的表查询,以便收集所有联系人。
- 然后,通过调用CloudTable实例的execute方法来执行表查询。
- 循环遍历并构建一个ContactEntity类型的ArrayList以供返回。
private ArrayList<ContactEntity> FetchAll(){ ArrayList<ContactEntity> allContacts = new ArrayList<ContactEntity>(); try { CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString); // Create the table client. CloudTableClient tableClient = storageAccount.createCloudTableClient(); // Create a cloud table object for the table. CloudTable cloudTable = tableClient.getTableReference(tableName); TableQuery<ContactEntity> partitionQuery = TableQuery.from(ContactEntity.class); for (ContactEntity entity : cloudTable.execute(partitionQuery)) { allContacts.add(entity); } } catch (Exception e) { // Output the stack trace. e.printStackTrace(); } return allContacts; }
将数据绑定到TableView
现在是时候看看数据绑定是如何完成的了。以下是代码片段供参考。您会注意到,我们将所有联系人保存在一个ContactEntityTableView类型的ObservableList中。
ObservableList<ContactEntityTableView> contactData; contactData = FXCollections.observableArrayList(); ArrayList<ContactEntity> allContacts = FetchAll(); allContacts.stream().forEach((contact) -> { SetObservableCollection(contact); }); tableView.setItems(contactData);
ContactEntityTableView是一个特殊的实体,我们用它将所有联系人绑定到TableView控件。以下是相应的代码片段。请注意我们使用的SimpleStringProperty。我们与ObservableList一起使用的所有属性都必须定义为这种类型。
public class ContactEntityTableView { public SimpleStringProperty firstName = new SimpleStringProperty(); public SimpleStringProperty lastName = new SimpleStringProperty(); public SimpleStringProperty email = new SimpleStringProperty(); public SimpleStringProperty phoneNumber = new SimpleStringProperty(); public String getEmail() { return this.email.get(); } public String getPhoneNumber() { return this.phoneNumber.get(); } public String getFirstName() { return this.firstName.get(); } public String getLastName() { return this.lastName.get(); } }
在下面的代码片段中,您可以看到如何基于ContactEntity构建ObservableList。
private void SetObservableCollection(ContactEntity contact){ ContactEntityTableView tableViewEntity = new ContactEntityTableView(); tableViewEntity.firstName.set(contact.getFirstName()); tableViewEntity.lastName.set(contact.getLastName()); tableViewEntity.email.set(contact.getEmail()); tableViewEntity.phoneNumber.set(contact.getPhoneNumber()); contactData.add(tableViewEntity); }
发送短信
在通过Twilio发送短信时,有一件重要的事情需要理解。只有当你拥有Twilio号码或需要迁移一个号码时,你才能发送短信。也就是说,你不能像我们用于语音通话那样使用你的Twilio已验证号码。
获取Twilio号码非常简单,但他们每月至少收费1美元。
这是“发送短信”屏幕的快照。
让我们看看如何发送短信。以下是相应的代码片段。
- 我们首先需要做的是创建一个TwilioRestClient实例。
- 基于上述客户端实例获取一个Account实例。
- 基于账户实例获取SmsFactory实例。
- 创建一个NameValuePair(NameValuePair)类型的列表,并添加一个BasicNameValuePair类型的实例,名称为“To”,值为目标电话号码。同样,我们也为“from”电话号码这样做。
- 通过调用SmsFactory实例的create方法并传入NameValuePair实例列表,将触发一条短信。
private void SendSMS(String from, String to, String message) { if( !"".equals(from) && !"".equals(to) && !"".equals(message)){ TwilioRestClient client = new TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN); Account mainAccount = client.getAccount(); final SmsFactory messageFactory = mainAccount.getSmsFactory(); final List<NameValuePair> messageParams = new ArrayList<>(); messageParams.add(new BasicNameValuePair("To", from)); // Replace with a valid phone number messageParams.add(new BasicNameValuePair("From", to)); // Replace with a valid phone number in your account, must be a Twilio Number messageParams.add(new BasicNameValuePair("Body", message)); try { messageFactory.create((Map<String, String>) messageParams); } catch (TwilioRestException ex) { Logger.getLogger(SMSController.class.getName()).log(Level.SEVERE, null, ex); } } }
搜索已保存的联系人
现在我们将介绍搜索联系人功能。之前,我们已经看到联系人是如何保存在Azure表存储中的。我们将通过姓和/或名来搜索联系人。
这是我们的搜索屏幕的快照。
以下是代码片段,您可以在其中看到Search函数接受两个参数:firstname和lastname。
如果您看到下面的代码,它与我们之前获取所有联系人时的操作非常相似。但我们添加了用于firstname和lastname的条件过滤器,并通过执行分区查询来过滤我们的表存储。
请注意,下面是如何为PARTITION_KEY和ROW_KEY构建搜索过滤器,并精确匹配姓名。
private ArrayList<ContactEntity> Search(String firstName, String lastName){ ArrayList<ContactEntity> allContacts = new ArrayList<>(); final String PARTITION_KEY = "PartitionKey"; final String ROW_KEY = "RowKey"; try { // Retrieve storage account from connection-string. CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString); // Create the table client. CloudTableClient tableClient = storageAccount.createCloudTableClient(); // Create a cloud table object for the table. CloudTable cloudTable = tableClient.getTableReference(tableName); String partitionFirstNameFilter = TableQuery.generateFilterCondition( PARTITION_KEY, TableQuery.QueryComparisons.EQUAL, firstName); String partitionLastNameFilter = TableQuery.generateFilterCondition( ROW_KEY, TableQuery.QueryComparisons.EQUAL, lastName); String sCombinedFilters = TableQuery.combineFilters(partitionFirstNameFilter, Operators.AND, partitionLastNameFilter); if(!"".equals(firstName) && "".equals(lastName)){ partitionQuery = TableQuery.from(ContactEntity.class) .where(partitionFirstNameFilter); }else if("".equals(firstName) && !"".equals(lastName)){ partitionQuery = TableQuery.from(ContactEntity.class) .where(partitionLastNameFilter); } else{ partitionQuery = TableQuery.from(ContactEntity.class) .where(sCombinedFilters); } if(partitionQuery != null){ for (ContactEntity entity : cloudTable.execute(partitionQuery)) { allContacts.add(entity); } } } catch (Exception e) { // Output the stack trace. e.printStackTrace(); } return allContacts; }
编写WebAPI
注意 - 在开始之前,让我说清楚。它不一定非得是WebAPI,但我选择它是因为我可以用它轻松编写代码。您也可以选择HTTP Handlers或其他服务后端技术来返回Twilio响应。
如前所述,当您尝试通过使用Twilio已验证号码或自有号码发起呼叫时,有一个URL参数需要设置。这个URL本质上是一个服务的地址,它向Twilio提供指令,告诉Twilio确切地要做什么。Twilio有一个解析器来解析TwiML指令。您可以将这些指令视为Twilio的命令。
我们一步一步来。我们将编写一套非常简单的指令。下面是ApiController的代码片段,它实现了一个HTTP Post方法,您可以在其中看到我们如何构建TwilioResponse。
注意 - 您需要从Nuget包管理器安装Twilio库。搜索“Twilio”并安装Twilio.Mvc和Twilio.TwiML。
public class CodeProjectDemoController : ApiController { public HttpResponseMessage Post(VoiceRequest request) { var response = new TwilioResponse(); response.Say("Welcome to CodeProject Twilio App. Please enter your 5 digit ID."); response.Gather(new { numDigits = 5 }); return this.Request.CreateResponse(HttpStatusCode.OK, response.Element, new XmlMediaTypeFormatter()); } }
Twilio提供了一系列有用的指令/命令供我们使用。这是Twilio文档中的命令集。有关更多详细信息,您可以参考以下链接 https://www.twilio.com/docs/api/twiml
- Say - 向呼叫者朗读文本
- Play - 为呼叫者播放音频文件
- Dial - 将另一方添加到通话中
- Record - 录制呼叫者的声音
- Gather - 收集呼叫者在键盘上输入的数字
- Sms - 在电话呼叫期间发送短信
- Hangup - 挂断电话
- Queue - 将呼叫者添加到呼叫者队列中
- Redirect - 将呼叫流重定向到不同的TwiML文档。
- Pause - 在执行更多指令之前等待
- Reject - 拒绝来电而不收费。
从上面的代码可以看出,我们使用了“Say”和“Gather”命令,并构建了一个TwilioResponse。如果我们设置URL参数来使用CodeProjectDemoController,然后尝试拨打一个电话,例如将“To”号码设置为您的一个手机号码进行测试,那么您将听到以下内容。
"**欢迎来到CodeProject Twilio应用程序。请输入您的5位ID**"。这来自Say命令。还有一个用于收集数字的指令;它最多会收集5个数字,如Gather命令的numDigits属性所示。
现在,让我们看看如何在您希望将一个电话连接到源为您号码、目标为您朋友号码时发起呼叫。以下是相应的代码片段。
请注意下面的“Dial”命令的使用,我们将拨打目标电话号码,并将呼叫者ID指定为源电话号码,这样任何接到电话的人都能确切地知道是谁在呼叫他们。
我们不必担心电话是如何从源连接到目标的,这都是由Twilio处理的。
public class InitiateCallController : ApiController { public HttpResponseMessage Get(string source, string target) { var response = new TwilioResponse(); response.Dial(target, new { callerId = source }); return this.Request.CreateResponse(HttpStatusCode.OK, response.Element, new XmlMediaTypeFormatter()); } }
关注点
我一直知道Twilio和其他一些提供商的工作方式有些相似。但从未有机会深入探索,进行集成和构建一些有用的东西。
在我写这篇文章的时候,我才意识到使用API进行VOIP通信的全部潜力。它不仅仅是连接两个人或多人之间的电话,还有更多,例如应用程序的回拨,甚至我们可以发起一个呼叫并根据语音指令执行某些操作,并根据我们说的话或输入的数字接收反馈。当我了解到使用Twilio,不仅可以拨打电话,还可以接听电话,这真是太神奇了,您可以想象这些API在构建可以与人类交互的应用程序中的潜在用例。
总而言之,所有物联网设备都应该并且必须能够与我们进行通信和交互。
参考文献
本项目重用了PiPhone项目中的电话号码图标。
https://github.com/climberhunt/PiPhone/tree/master/icons
历史
V1.0 - 发布文章初稿,日期为2015年2月18日。