树莓派与iOS交互应用

构建与树莓派交互的应用程序

关于您的树莓派

树莓派是一种微型ARM计算机,可以运行不同的操作系统。默认情况下,它运行为此设备定制的Debian Linux变体,称为Raspbian。在树莓派2上,您还可以安装Ubuntu、RiscOS,以及最近推出的精简版定制Windows 10。当然,我们必须意识到其CPU性能并不强大,且树莓派设备内存有限:B型配备512MB内存,而最新版本(Pi 2)拥有1GB内存,而这恰好是 ARM7 CPU支持的最大内存容量。这显然不足以进行复杂的计算任务:截至目前,尚无简便方法安装主流浏览器。虽然支持chromium浏览器,但Google Chrome尚不可用;即使可用,其资源占用也过大,超出当前系统资源能力,无法像普通计算机那样执行您习惯的操作。

树莓派在计算性能上的不足,通过极低的功耗以及连接智能设备和配件(如LED灯、继电器、电机、摄像头,以及压力、加速度和湿度传感器等)的能力得到了弥补。

另一个因素是价格:普通的树莓派B型售价约为30美元,而且您可以从许多线上和本地零售商处以非常实惠的价格购买到大多数扩展模块。

Pi 2 Model B 基于博通BCM2836系统级芯片,包含四核ARM7 900 MHz处理器、1GB内存和4个USB端口,可用于连接外部设备。该Pi设备还配有以太网接口、用于显示屏的 HDMI接口以及音频接口。较早的Pi B型+采用性能较弱的单核博通BCM2835 ARM11 700MHz处理器,仅有512MB内存,而最早的型号仅配备256MB内存。

要为树莓派供电,您需要通过微型USB将其连接到任何能够为树莓派及所连设备提供足够电力的墙壁充电器或计算机。在大多数情况下,1安培的电流就已足够,这相当于普通USB端口提供的电量。除了主板本身的功耗外,还需加上插件板所消耗的电流。树莓派不包含内置硬盘或固态驱动器,而是依赖microSD卡进行启动和长期存储。

树莓派没有配备实时时钟,因此操作系统必须使用网络时间服务器,或在启动时向用户询问时间信息,以获取时间和日期信息,从而实现+文件的时间和日期标记。然而,可以通过 I2C接口轻松添加带有电池备份的实时时钟(例如DS1307)。

树莓派还提供了两个板载带状插槽,用于连接摄像头和显示屏。大多数用于安装树莓派的塑料外壳都会预留孔位,以便将这些线缆引出到外壳外部。

为了适应本章节的范围,我们选择了一个非常简单的模块,该模块在一块微型电路板上配有多个LED灯以及控制这些LED灯的芯片。该模块名为“PiGlow”,由Pimoroni公司销售:
https://shop.pimoroni.com/products/piglow

示意图0

如果你拥有较早版本的树莓派,你仍然可以完成本章节中的所有操作;新版本的优势仅在于更快的处理器和更大的内存。我们将运行的一些命令特定于你所拥有的树莓派型号;内联注释将说明这些差异。

树莓派上的控制界面

每个嵌入式平台都有一种或多种方法来访问本地硬件上的连接设备或原生设备。

一个常见的例子是GPIO(通用输入输出)。它允许你打开或关闭灯或继电器,对于这类用途非常理想;但其可寻址的设备数量有限(大致受限于自定义连接器上可用引脚的数量)。

I2C(集成电路间)接口让您在使用单个设备时拥有更大的灵活性,当然也允许您连接更多设备。

I2C 总线由飞利浦在20世纪80年代初设计,旨在方便同一电路板上各组件之间的通信。有关 I2C 接口的完整参考,请参见以下 URL:www.i2c-bus.org/i2c-bus/。

I2C 使用 7位寻址。用 7 位可以表示的最大数值是 128,这意味着您可以通过同一总线访问超过 120 个设备。I2C 在硬件上实现起来也简单得多——它只需要两条连接线,一条用于时钟,另一条用于数据。

设置你的树莓派

开始使用树莓派最简单的方法是安装 Raspbian 操作系统。树莓派官方网站在此处提供下载: www.raspberrypi.org/downloads/。

为了方便起见,请使用 NOOBS(全新开箱软件)zip 文件。

在同一页面上还提供了安装说明;解压下载的zip文件后,使用FAT文件系统格式化一张容量较大的微型SD卡(4GB或更大),并将文件复制到该卡上。然后,将此卡插入您的树莓派设备并启动。当设备启动时,会提示您选择操作系统和语言,之后即可完成安装。

在安装阶段,您可以使用HDMI显示器或通过HDMI转VGA适配器连接的VGA显示器;此操作在后续步骤中将不再需要,因为您可以通过另一台计算机使用SSH(安全外壳)登录到该设备。

要从另一台计算机通过 SSH 登录到树莓派设备,您可以使用以下凭据:
user: pi password: raspberry

如果默认情况下未安装图形用户界面(GUI),可以随时使用以下命令启动图形用户界面:
startx

要配置设备和端口,可以使用树莓派配置工具:
raspi-config

安装操作系统后,第一步是将系统更新到最新状态。为此,你可以在SSH窗口中输入以下内容:
sudo apt-get update sudo apt-get upgrade

为了强制以根权限执行命令,我们在普通命令前使用sudo命令。这是必要的,因为标准的 “pi”用户没有足够的权限来修改系统文件或安装应用程序。

apt‐get 工具是标准的 Debian 包管理器,如果您使用的是 Ubuntu,可能对此比较熟悉。

第一个命令获取最新可用软件包的列表,而第二个命令则安装当前已安装的系统模块/软件包的可用更新。

选择脚本语言

在树莓派上,Python 已成为事实上的标准脚本语言,因为有大量软件包可供控制各种设备。但需要注意的是,其中大多数是 Python 2 的,也有一些是 Python 3 的。Python 语言具有双重特性:Python 2 是较旧且更成熟的版本,拥有更广泛的支持;Python 3 是新的“更好”的版本,提供了面向对象设计的现代语言结构,但在树莓派等平台上的支持有限,并且为 Python 3 编写的脚本和软件包无法在 Python 2 上运行。

同时,嵌入式平台对 Perl 有相当大的支持;然而,安装某些 Perl 软件包可能是一项艰巨的任务,大多数新手都很难完成。

配置I2C

本节中的大部分说明都可以在以下 URL 找到,在该网址您还可以找到更多关于如何为您的树莓派安装其他配件和插件的教程:
https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c

I2C 是一种非常常用的标准,旨在允许一个芯片与另一个芯片进行通信。

由于树莓派可以与 I2C 通信,因此我们可以将其连接到各种支持 I2C 的芯片和模块。

接下来是一些利用 I2C设备 和 模块 的 Adafruit 项目 :
- http://learn.adafruit.com/mcp230xx-gpio-expander-on-the-raspberry-pi
- http://learn.adafruit.com/adafruit-16x2-character-lcd-plus-keypad-for-raspberry-pi
- http://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi
- http://learn.adafruit.com/matrix-7-segment-led-backpack-with-the-raspberry-pi
- http://learn.adafruit.com/mcp4725-12-bit-dac-with-raspberry-pi
- http://learn.adafruit.com/adafruit-16-channel-servo-driver-with-raspberry-pi
- http://learn.adafruit.com/using-the-bmp085-with-raspberry-pi

I2C 总线允许将多个设备连接到您的树莓派——每个设备都有唯一的地址。通常可以通过更改模块上的跳线设置来设定设备地址。能够查看哪些设备连接到了您的树莓派,有助于确认一切正常工作,这非常有用。为此,值得在终端中运行以下命令以安装 i2c-tools 工具:
sudo apt-get install i2c-tools

为了简化操作,我们将使用 Python 编写适用于树莓派的应用程序。要在 Python 中使用 I2C 工具,我们需要安装 python‐smbus 软件包:
sudo apt-get install python-smbus

如果计划使用Perl构建控制I2C设备的工具,则需要安装一些额外的软件包,这些软件包为 Perl软件包提供对I2C工具的接口,以及允许编写紧凑型应用程序的基本框架:
sudo apt-get install libi2c-dev build-essential libmoose-perl sudo cpan Device::SMBus

安装内核对 I2C 的支持

以 root 身份运行 raspi‐config,并按照提示为 ARM核心 和 Linux内核 安装 i2c支持:
sudo raspi-config

选择高级选项/I2C并启用接口,允许默认加载I2C内核模块。更改后,必须重新启动设备;重启后模块应已加载并可用。

验证 t 的 i2c模块 可用,通过查看以下文件的内容 e:
cat/etc/modules

该文件的内容将类似于以下示例:

#/etc/modules: kernel modules to load at boot time.#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with"#" are ignored.

i2c-bcm2708
i2c-dev

如果重启后,这些模块不存在于 /etc/modules 文件中:你可以使用 vi 编辑该文件并添加前面提到的两行。你必须以根权限编辑此文件:
sudo vi/etc/modules

另一种简单的方法是使用以下单行命令:
sudo echo”i2c-bcm2708”>>/etc/modules
sudo echo”i2c-dev”>>/etc/modules

根据您安装操作系统的时间、版本,以及您可能在树莓派上进行的其他实验,其中一些模块可能已被列入modprobe黑名单。这是一个禁用特定模块的文件,防止它们在the启动时from加载ing 。要查看文件内容,请参见:
sudo cat/etc/modprobe.d/raspi-blacklist.conf

你可以使用 vi 编辑该文件,如果文件中存在这些行,则将其注释掉或删除,就像我们对 /etc/modules 所做的那样。同样,你需要使用根权限来编辑该文件:
sudo vi/etc/modprobe.d/raspi-blacklist.conf

有一种更高级的方法可以通过以下单行命令实现几乎相同的功能:
MPBL=/etc/modprobe.d/raspi-blacklist.conf;[-f${MPBL}]&& sudo perl-p-i-e ‘s:^(blacklist(spi|i2c)-bcm2708):#$1:g’${MPBL}

这一行代码执行以下操作:
- 创建一个名为MPBL的环境变量,该环境变量包含指向文件的路径
- 如果文件存在,则使用 Perl 查找包含两个模块名称的行
- 如果找到了这些行,则通过在前面加上井号来注释掉它们

虽然运行一行命令来为你完成某些操作看起来非常炫酷,但更方便的方式是采用常规方法,即在普通文本编辑器中编辑一个文件,并立即看到你正在做的事情。

之后进行之前提到的更改后,你可以重新启动你的树莓派 i:
sudo reboot

验证 I2C 是否可访问

设备重启后,第一步是检查模块是否已加载。
sudo modprobe i2c-dev
如果模块无法加载,将返回“ modprobe: FATAL: Module i2c-dev not found”,否则不会返回任何消息。

如果一切正常,你应该能够通过运行以下命令来测试I2C连接 :
i2cdetect -y 1

如果 h如果是较早版本的树莓派(B型之前),则应使用以下命令:
i2cdetect -y 0

树莓派的制造商在B型发布时将那个0改为了1。一个简单的记忆方法是,任何带有256MB内存的模块都使用0;其余的则使用1。

无论哪种情况,该命令的输出都类似于以下内容:

0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: --------------------------
10:--------------------------------
20:--------------------------------
30:--------------------------------
40:--------------------------------
50:--------------------------------
60:--------------------------------
70:----------------

如果已经连接了I2C设备,它们可能会出现在表格中,并指向您的设备地址。

一个有趣的事实:如果你已插入PiGlow模块,它不会出现在i2cdetect命令的输出中,但仍然可以正常工作。

如果运行的是较新的树莓派(3.18内核或更高版本),则还需要更新/boot/config.txt文件。

编辑/boot/config.txt文件,取消注释或添加以下行:

dtparam=i2c1=on
dtparam=i2c_arm=on

完成这些更改后,你需要重新启动你的设备。

配置GPIO

GPIO引脚既可以用作数字输出,也可以用作数字输入。作为数字输出时,您可以编写程序将特定引脚设置为高电平或低电平。设置为高电平时,其电压为3.3伏特;设置为低电平时,其电压为0伏特。

要从其中一个引脚驱动LED灯,您需要串联一个1千欧电阻,因为GPIO引脚只能承受较小的功率。

如果将引脚用作数字输入,则可以将开关和简单传感器连接到引脚,然后检测其是否处于打开或关闭状态(即是否被激活)。以下是一些仅使用GPIO的Adafruit项目:
- http://learn.adafruit.com/raspberry-pi-e-mail-notifier-using-leds
- http://learn.adafruit.com/playing-sounds-and-using-buttons-with-raspberry-pi
- http://learn.adafruit.com/basic-resistor-sensor-reading-on-raspberry-pi

要在Python中编程控制GPIO端口,我们需要安装一个非常有用的Python 2库,名为RPi.GPIO(树莓派通用输入输出)。该模块为我们提供了一个简单易用的Python库,使我们能够控制GPIO引脚。无论你使用的是Raspbian还是Occidentalis系统,此库的安装过程都相同。事实上,某些版本的 Raspbian 包含此库,但这些说明还将更新到最新版本,这是值得做的。

要安装RPi.GPIO库,首先需要安装RPi.GPIO库所需的Python开发工具包。为此,请在 LXTerminal中输入以下命令:
sudo apt-get install python-dev

然后,安装 RPi.GPIO库 本身,请输入:
sudo apt-get install python-rpi.gpio

安装 PyGlow

为了让 PyGlow 模块与 Python 协同工作,我们需要安装一个能够与 I2C库 交互并控制设备的软件包。
sudo pip install git+https://github.com/benleb/PyGlow.git

现在,你可以创建一个小型的 Python 测试脚本,让灯先蓝色闪烁 1 秒,再红色闪烁 2 秒,最后绿色闪烁 3 秒(见代码清单 12-1)。

清单12-1. 闪烁PiGlow LED灯

from PyGlow import PyGlow
from time import sleep

pyglow = PyGlow()
pyglow.all(0)
pyglow.color("blue", 100)
sleep(1)
pyglow.color("blue", 0)
pyglow.color("red", 100)
sleep(2)
pyglow.color("red", 0)
pyglow.color("green", 100)
sleep(3)
pyglow.color("green", 0)

将清单12‐1中的文本保存到名为flash.py的文件中,然后在SSH终端中运行它:
python flash.py

就是这样;很简单,对吧?

现在设想一下,你将编写的更复杂的控制命令不过是像清单 12-1 中那样的封装脚本,该脚本控制特定的接口和设备并实现某些更改。

提供一个用于控制设备的API

到目前为止,我们已经了解到可以与树莓派上连接的设备进行交互。而现在我们希望可以从外部源(例如我们的 iOS 设备)与这些设备进行交互。

这部分包括两个部分:一个是运行在树莓派上的服务器,它提供用于控制连接设备(在本例中为PiGlow)的API;另一个是向该API发送请求的iOS应用程序。

自从我们开始编写Python代码以来,用Python来开发API服务器似乎也是顺理成章的。有许多Python框架可用于构建API:我们需要选择一个轻量且易于定制的框架。这里的一个不错选择是Flask。你会发现网上有大量的教程帮助你入门Flask,并且作为API开发框架,你会体会到上手是多么容易。

安装Flask

这是一个非常简化的 Flask 入门分步教程。我们实现了一些基本功能,以允许我们控制设备。

在开始之前,我们使用 Pip 安装 Flask:

sudo pip install flask

这将安装Flask框架以及该框架使用的基本模块。安装过程看似简单,但实际上与其他包的安装方式相同。

Hello World守护进程

这传统上是我们开始尝试一门新语言时首先要做的事情。

创建一个名为 hello-flask.py的文件,内容如清单 12-2所示。通常,由于该服务提供 HTTP 响应,因此应将其配置为使用 80端口。

我们使用较高的端口号出于两个原因:一是允许你在需要时在80端口运行单独的HTTP服务器,尤其是因为小于1024的端口只能由具有根权限的程序使用。这是出于安全考虑的一个较早的限制,它确保在某些知名端口上运行的应用程序只能由系统运行。

清单 12-2. 使用 Flask 的 Python “你好,世界!” 脚本
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080, debug=True)

当你从命令行启动守护进程时,它会告诉我们它正在运行,并在命令行中显示任何传入的调用。

清单 12-3 是我们首次启动时得到的结果。你会注意到浏览器还试图获取 favicon.ico文件,这个文件我们并没有,因为本示例中构建的守护进程未配置为直接提供静态文件。 favicon.ico是用于自定义网站图标在浏览器中URL左侧显示样式的图像,该请求仅会出现一次,之后浏览器将缓存该状态,不再尝试重新获取。

清单12-3. 运行我们的Flask程序
pi@raspberrypi:~$ python hello-flask.py 
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
 * Restarting with stat
10.0.1.25 -- [12/Oct/2015 05:26:18] "GET / HTTP/1.1" 200 -
10.0.1.25 -- [12/Oct/2015 05:26:18] "GET /favicon.ico HTTP/1.1" 404 -

清单 12-3 中最后两行是使用设备的 IP(互联网协议)地址加载设备 URL 的结果:

http://10.0.1.128:8080/

在我们的示例代码中,我们不会去设置发现守护进程或任何高级功能,仅限于演示功能。

构建一个非常简单的监听守护进程

通过从该测试程序中积累的知识,现在是时候集成一个调用来执行我们在前面示例中编写的命令了。我们要构建的是一个守护进程,它可以告诉我们系统时间,并提供执行简单命令的服务。

由于我们已经在前面的示例中编写了一些代码,我们将把这些代码集成到我们的监听器中。你可以在清单12-4中看到结果。

你会注意到,我们将所有导入都放在了顶部,整合了该脚本中编写的所有代码所需的内容。

清单12-4. 在树莓派上使用Python和Flask编写的守护进程
from flask import Flask
from PyGlow import PyGlow
from time import sleep
import datetime

app = Flask(__name__)

@app.route("/")
def hello():
    now = datetime.datetime.now()
    return now.strftime("%Y-%m-%d %H:%M")

@app.route("/blink")
def getData():
    pyglow = PyGlow()
    pyglow.all(0)
    pyglow.color("blue", 100)
    sleep(1)
    pyglow.color("blue", 0)
    pyglow.color("red", 100)
    sleep(2)
    pyglow.color("red", 0)
    pyglow.color("green", 100)
    sleep(3)
    pyglow.color("green", 0)
    return "OK"

@app.route("/blink/<color>")
def blinkColor(color):
    pyglow = PyGlow()
    pyglow.all(0)
    pyglow.color(color, 100)
    sleep(1)
    pyglow.color(color, 0)
    return "OK"

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080, debug=True)

至此,我们创建了第一个非常简单的命令处理守护进程。现在尝试打开基础URL,它将显示当前日期和时间:

http://10.0.1.128:8080/

这个Flask守护进程现在会在有人调用/blink端点时,接收命令以使PiGlow上的LED灯闪烁。

当调用该URL时,你会看到我们的代码正在执行,LED灯会像之前示例中那样闪烁;执行完成后,页面将显示“OK”。

http://10.0.1.128:8080/blink

我们还有第二个服务,允许我们闪烁单色:为此,请调用以下URL:

http://10.0.1.128:8080/blink/green
http://10.0.1.128:8080/blink/red

listener 守护进程会在命令行中显示正在进行的调用 :

10.0.1.25 -- [12/Oct/2015 06:04:44] "GET /blink/green HTTP/1.1" 200 -
10.0.1.25 -- [12/Oct/2015 06:05:03] "GET /blink/red HTTP/1.1" 200 -

我相信您在尝试添加新命令和功能时会获得很多乐趣。

为我们的应用程序设置iOS项目

我们首先创建一个空的单页项目。本章节旨在展示如何通过我们刚刚创建的I2C接口与树莓派 API通信,而不是介绍如何围绕它构建用户界面,因此我们的应用程序将极为简洁,仅暴露少数几个用户界面元素来触发树莓派上的操作。我们将采用与第5章中代码类似的方法,因此如果你阅读过该章节,这部分内容将会很熟悉。

使用此演示应用程序,您将能够触发命令,以打开和关闭树莓派上PiGlow板的灯光。

允许发出HTTP调用

你会经常遇到这种情况:你设置应用程序发起一个 HTTP 调用,然后看到类似如下的堆栈跟踪:

应用传输安全已阻止加载不安全的明文HTTP(http://)资源。临时例外可通过应用程序的 Info.plist文件进行配置。

前往Apple文档,我们了解到关于应用传输安全的内容:

应用程序传输安全(ATS)允许应用程序在其Info.plist文件中添加声明,以指定需要进行安全通信的域名。ATS可防止意外泄露,提供安全的默认行为,并且易于采用。无论您是创建新应用程序还是更新现有应用程序,都应尽快采用ATS。

如果你正在开发一个新的应用程序,你应该 exclusively 使用 HTTPS。如果你拥有一个现有的应用程序,则应尽可能立即使用 HTTPS,并制定计划尽快迁移应用程序的其余部分。

为了解决此问题,你需要在 info.plist 中创建一个“允许任意加载”设置为“true”的条目,如图 12-2 所示。当你开始键入时,Xcode 会自动建议名称(应用传输安全设置),它还会建议字典类型以及第一个键值对(允许任意加载)。你可以在图 12-2 中看到其显示方式。

示意图1

视图控制器

本章节的基本视图控制器将仅显示几个按钮和一个文本区域,我们将使用该文本区域来显示与 API 的通信。

为了初始化并能够使用这些按钮和字段,必须为它们分配宏,以在界面构建器中使其可用/可见。我们还定义了用于API和日志记录对象的变量。由于这些变量将在稍后初始化,因此需要将它们定义为可选类型(清单 12-5)。

清单12-5. UIViewController类的头文件
class ViewController: UIViewController {
    @IBOutlet var clearButton: UIButton!
    @IBOutlet var labelButton: UIButton!
    @IBOutlet var labelButton2: UIButton!
    @IBOutlet var textArea: UITextView!

    var api: APIClient!
    var logger: UILogger!
}

viewDidLoad() 函数(代码清单 12-6)中,我们初始化了API对象以及日志库,后者会将文本输出到我们的文本区域字段。这些库的内容和功能将在后续进行解释。

清单12-6. viewDidLoad重写函数
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    api = APIClient(parent: self)
    logger = UILogger(out: textArea)
}

要为按钮分配一个操作,我们需要创建一个执行该操作的函数,并使用适当的宏进行注解,使其在界面构建器中可用。我们将在请求开始时添加一条日志语句,还可以在按钮被按下时更改其标题(参见清单 12-7):

清单12-7. clickButton函数
@IBAction func clickButton() {
    logger.logEvent("=== Blink All Lights ===")
    api.blinkAllLights()
    labelButton.setTitle("Request Sent", forState: UIControlState.Normal)
}

我们可以在故事板中将这些按钮操作连接起来,如图 12-3 所示。

示意图2

在清单 12-8 中,我们可以看到用于测试发送到树莓派 API 的命令的完整 ViewController.swift代码。

清单12-8. ViewController.swift 的代码
import UIKit

class ViewController: UIViewController {
    @IBOutlet var clearButton: UIButton!
    @IBOutlet var labelButton: UIButton!
    @IBOutlet var labelButton2: UIButton!
    @IBOutlet var textArea: UITextView!

    var api: APIClient!
    var logger: UILogger!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        api = APIClient(parent: self)
        logger = UILogger(out: textArea)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func unclickButton() {
        labelButton.setTitle("Blink All Lights", forState: UIControlState.Normal)
    }

    @IBAction func unclickButton2() {
        labelButton2.setTitle("Blink Red Light", forState: UIControlState.Normal)
    }

    @IBAction func clickButton() {
        logger.logEvent("=== Blink All Lights ===")
        api.blinkAllLights()
        labelButton.setTitle("Request Sent", forState: UIControlState.Normal)
    }

    @IBAction func clickButton2() {
        logger.logEvent("=== Blink Red Light ===")
        api.blinkLight("red")
        labelButton2.setTitle("Request Sent", forState: UIControlState.Normal)
    }

    @IBAction func clickClearButton() {
        logger.set()
    }
}

日志记录库

在视图控制器中,日志记录库被分配给一个变量,该变量将保留一个已分配适当目标的日志记录器实例;在我们的例子中,我们使用文本区域字段进行活动日志记录。

当主线程更新用户界面元素时,无需特殊处理。但由于写入此日志的调用是在子线程中运行的,因此我们确保对用户界面元素的更新将作为异步操作进行分发(见代码清单 12-9)。

清单12-9. 分发异步事件
func set(text: String? = "") {
    dispatch_async(dispatch_get_main_queue()) {
        self.textArea!.text = text
    };
}

为了简化操作,我们仅实现几个函数,用于跟踪 API 活动。这些函数将与我们在视图控制器中设置的文本区域字段进行交互。与视图控制器中一样,文本区域字段被声明为可选类型,因为它将在 init()函数中初始化。你可以在清单 UILogger.swift文件中查看完整的代码,如代码清单 12-10所示。

清单12-10. UILogger库
import Foundation
import UIKit

class UILogger {
    var textArea: UITextView!

    required init(out: UITextView) {
        dispatch_async(dispatch_get_main_queue()) {
            self.textArea = out
        };
        self.set()
    }

    func set(text: String? = "") {
        dispatch_async(dispatch_get_main_queue()) {
            self.textArea!.text = text
        };
    }

    func logEvent(message: String) {
        dispatch_async(dispatch_get_main_queue()) {
            self.textArea!.text = self.textArea!.text.stringByAppendingString("=>" + message + "\n")
        };
    }
}

API客户端库

现在我们将创建 APIClient.swift library。该库用于向 API 发送异步请求的函数。类的头部包含 API 功能所需的 URL 和其他变量。

定义设备URL

您可以创建一个弹出屏幕在设备上输入此信息,或者可以使用Bonjour编写一个发现服务来发现该设备。为了简化操作,我们将设备的IP地址和端口作为变量进行硬编码(见清单12-11)。

清单12-11. APIClient 库的头部
import Foundation

class APIClient {
    var apiVersion: String!
    var baseURL: String = "http://10.0.1.128:8080"
    var viewController: ViewController!

    required init(parent: ViewController!) {
        viewController = parent
    }
}

创建一个GET处理器

一个用于从服务执行GET的通用函数如清单12‐12中的代码所示。该代码是APIClient类的一部分,我们将它保存为APIClient.swift文件。

清单12-12. 用于执行GET的通用函数
func getData(service: APIService, id: String! = nil, urlSuffix: NSArray! = nil, params: [String:String]! = [:]) {
    let blockSelf = self
    let logger: UILogger = viewController.logger

    self.apiRequest(
        service,
        method: APIMethod.GET,
        id: id,
        urlSuffix: urlSuffix,
        inputData: params,
        callback: {(responseJson: NSDictionary!, responseError: NSError!) -> Void in
            if (responseError != nil) {
                logger.logEvent(responseError!.description)
                // Handle here the error response in some way
            } else {
                blockSelf.processGETData(service, id: id, urlSuffix: urlSuffix, params: params, responseJson: responseJson)
            }
        })
}

对于 urlSuffix,我们使用 NSArray 数据类型来存储被访问 URL 的所有元素。由于我们不知道将向 API 发送什么数据,因此 NSArray 类型非常合适,因为它默认包含 AnyObject 元素。我们还将 urlSuffix 传递给 processGETData函数,以便根据所调用的服务、项目的可选 ID 以及 urlSuffix 来决定如何处理响应。我们还为 urlSuffix 和 params 定义了默认值,以便我们的函数可以在不提供所有 nil 参数的情况下进行调用。

可选输入参数是一个键和值均为字符串的字典。这是最方便的格式,因为从参数传递给 API的方式来看,POST与GET并无区别。

传递给 NSURLConnection.sendAsynchronousRequest 的代码块是一个闭包,这就是为什么我们需要分配将在 APIClient 库上下文中用于调用的 blockSelf 变量。

该函数是该响应的实际处理器,其通用形式如清单 12-13 所示。

清单12-13. 实现 GET 请求处理器
func processGETData(service: APIService, id: String!, urlSuffix: NSArray!, params: [String:String]! = [:], responseJson: NSDictionary!) {
    // do something with data here
}

创建 POST 处理器

与 GET 请求一样,POST 请求也可以具有清单 12-14 中所示的相同结构。这同样属于 APIClient 类(即 APIClient.swift 文件)。

清单12-14. 实现 POST 请求处理器
func postData(service: APIService, id: String! = nil, urlSuffix: NSArray! = nil, params:[String:String]! = [:]) {
    let blockSelf = self
    let logger: UILogger = viewController.logger

    self.apiRequest(
        service,
        method: APIMethod.POST,
        id: id,
        urlSuffix: urlSuffix,
        inputData: params,
        callback: {(responseJson: NSDictionary!, responseError: NSError!) -> Void in
            if (responseError != nil) {
                logger.logEvent(responseError!.description)
                // Handle here the error response in some way
            } else {
                blockSelf.processPOSTData(service, id: id, urlSuffix: urlSuffix, params: params, responseJson: responseJson)
            }
        })
}

func processPOSTData(service: APIService, id: String!, urlSuffix: NSArray!, params: [String:String]! = [:], responseJson: NSDictionary!) {
    // do something with data here
}

当然,我们可以用多种不同的方式来实现请求过程,但为API请求类型提供一个通用的处理器,能够帮助我们避免回调地狱。

定义动词和服务

我们注意到,这里的动词不是字符串,而是一个枚举值:APIMethod.GET。这是我们在该库中定义的一个枚举,用于方便地以字符串形式访问动词,而不是直接使用字符串。它还允许我们定义API客户端支持哪些HTTP动词(见代码清单12-15)。这同样也是APIClient类(位于APIClient.swift文件)的一部分。

清单12-15. 枚举 APIMethod
enum APIMethod {
    case GET, POST

    func toString() -> String {
        var method: String!

        switch self {
        case .GET:
            method = "GET"
        case .POST:
            method = "POST"
        }

        return method
    }
}

我们提供 hasBody( 函数作为示例,该函数可在 apiRequest 中用于正确格式化请求,使得 GET 和 DELETE 将参数用作键值对,而 PUT 和 POST 将其用作 JSON。

我们在 APIClient 库中定义了另一个枚举,通过 toString()函数 为实际服务提供快捷方式。

我们之前在视图控制器中见过这种用法,如 APIService.GOOD_JSON 所示。稍后我们将扩展此枚举以添加其他服务,并提供一个函数来返回某些调用可能需要使用的后缀,但目前清单 12-16 展示了基本格式。

清单12-16. APIService 枚举
enum APIService {
    case BLINK

    func toString() -> String {
        var service: String!

        switch self {
        case .BLINK:
            service = "blink"
        }

        return service
    }
}

为字符串类型添加扩展

我们在同一个 APIClient.swift文件中为字符串类型添加一个扩展,以增加一个简单的方法来转义可能需要在调用时传递的 URL参数(参见代码清单 12-17)。

清单12-17. 字符串对象的扩展
extension String {
    func escapeUrl() -> String {
        let source: NSString = NSString(string: self)
        let chars = "abcdefghijklmnopqrstuvwxyz"
        let okChars = chars + chars.uppercaseString + "0123456789.~_-"
        let customAllowedSet = NSCharacterSet(charactersInString: okChars)
        return source.stringByAddingPercentEncodingWithAllowedCharacters(customAllowedSet)!
    }
}

apiRequest() 函数

接下来要定义的是 apiRequest()函数(清单 12-18)。该函数将发起实际的API请求,并包含对响应数据的最终验证处理。方法签名表明,唯一必需的参数是服务、方法和回调函数。这也是 APIClient类的一部分。

清单12-18. apiRequest 函数
func apiRequest(
    service: APIService,
    method: APIMethod,
    id: String!,
    urlSuffix: NSArray!,
    inputData: [String:String]!,
    callback: (responseJson: NSDictionary!, responseError: NSError!) -> Void) {
    // Code goes here
}

当前可用的服务有INFO和BLINK:API会通过一组可变的参数对它们进行重载,因此本质上你的调用需要提供较大的APIService,然后通过urlSuffix提供URL路径扩展,以指向正确的资源。这将在后面进行更详细的说明。

关于方法的内容,以下是进行API请求时需要执行的操作:
1. 组成服务的基础URL
2. 如果指定了URL后缀,则添加URL后缀
3. 序列化输入参数并将其附加到URL
4. 以异步调用方式发起API请求

在传递给异步调用的代码块中,我们还需要执行以下操作:
1. 如果找到JSON,则反序列化JSON响应
2. 调用回调函数

要组合服务的基础URL,我们使用如清单 12-19 中所示的代码。

清单 12-19. 构建基础URL
var serviceURL = baseURL + "/"
if apiVersion != nil {
    serviceURL += apiVersion + "/"
}
serviceURL += service.toString()
if id != nil && !id.isEmpty {
    serviceURL += "/" + id
}
var request = NSMutableURLRequest()
request.HTTPMethod = method.toString()

在同一段代码中,我们创建了请求对象并为其分配了请求方法。此时服务URL仍在构建中,因此现在将其赋值给请求对象还为时过早。

如果此API支持POST请求的JSON请求体,我们可以使用12-20中的代码来序列化输入数据。

清单 12-20。序列化 JSON 数据
var error: NSError?
request.HTTPBody = NSJSONSerialization.dataWithJSONObject(inputData, options: nil, error: &error)
if error != nil {
    callback(responseJson: nil, responseError: error)
    return
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")

为了处理 URL 的组成,我们创建了 asURLString()函数。该函数位于 APIClient类中,并接收一个输入参数字典,生成一个经过 URL 编码的字符串,且参数按字母顺序排序(清单 12-21)。

清单12-21. asURLString 函数
func asURLString(inputData: [String:String]! = [:]) -> String {
    var params: [String] = []
    for (key, value) in inputData {
        params.append([key.escapeUrl(), value.escapeUrl()].joinWithSeparator("="))
    }
    params = params.sort { $0 < $1 }
    return params.joinWithSeparator("&")
}

URL后缀 需要成为 URL 的一部分——我们在输入中提供了一个字符串或数字的NSArray,用于组成该后缀——它们将全部被简化为一个简单的字符串,并附加到基础URL上。你可以在 postData()函数中的 APIClient类找到以下示例(清单 12-22):

清单12-22。构造一个URL
// The urlSuffix contains an array of strings that we use to compose the final URL
if urlSuffix?.count > 0 {
    serviceURL += "/" + urlSuffix.componentsJoinedByString("/")
}

现在我们可以准备将API请求作为异步调用来执行了。请注意,我们创建了一个指向视图控制器的日志处理程序的局部变量logger;这是必要的,因为在闭包内部,我们无法访问当前库或视图控制器中的变量和函数。异步调用的回调块包含了处理结果数据所需的基本代码,并调用我们在调用apiRequest()时获得的回调函数。同样,在解析响应时,可能会在解析JSON数据时发生错误,该错误将由回调函数进行处理。

要将API响应解析为JSON对象,我们使用一个NSDictionary对象来存储任意组合的键值对。

这是必要的,因为API响应可能包含数字、字符串、数组和字典的任意组合,而 NSDictionary默认支持AnyObject类型。NSJSONReadingOptions.MutableContainers指定数组和字典应创建为可变对象。我们可以在12-23中看到这一点。此代码位于postData()函数中的APIClient类中。

清单12-23. 解析JSON响应
var jsonResult: NSDictionary?

if urlResponse != nil {
    let rData: String = NSString(data: data!, encoding: NSUTF8StringEncoding)! as String
    if data != nil {
        do {
            try jsonResult = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
        } catch {
            // we expect an"OK" from the API, not JSON, so it's OK if we don't do anything here
        }
    }
}

当遇到需要报告的错误情况时,我们可以创建自己的错误对象。在 Swift 中,我们使用以下方法来实现:

error = NSError(domain: "response", code: -1, userInfo: ["reason": "blank response"])

我们为响应数据添加了一些日志记录,其中示例展示了如何将美化打印JSON输出到用于日志记录的文本区域。我们希望以易于阅读的方式格式化响应,而美化打印的JSON会以每个键值对占一行并进行适当的缩进。我们可以在12-24中看到结果。此代码位于postData()函数中的APIClient类。

清单12-24。处理REST调用
let logger: UILogger = viewController.logger
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { (data: NSData?, urlResponse: NSURLResponse?, error: NSError?) -> Void in
    // the request returned with a response or possibly an error
    logger.logEvent("URL: " + serviceURL)
    var error: NSError?
    var jsonResult: NSDictionary?

    if urlResponse != nil {
        let rData: String = NSString(data: data!, encoding: NSUTF8StringEncoding)! as String
        if data != nil {
            do {
                try jsonResult = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
            } catch {
                // we expect an"OK" from the API, not JSON, so it's OK if we don't do anything here
                // print("json error:\(error)")
            }
        }
        logger.logEvent("RESPONSE RAW: " + (rData.isEmpty ? "No Data" : rData))
        print("RESPONSE RAW: \(rData)")
    } else {
        error = NSError(domain: "response", code: -1, userInfo: ["reason": "blank response"])
    }
    callback(responseJson: jsonResult, responseError: error)
}
task.resume()

以美观的格式显示 JSON在其他地方也很有用,因此我们提取了 12-25 中的 prettyJSON()函数中的代码。此代码位于 postData()函数中,该函数属于 APIClient类。

清单12-25. 显示格式优美的JSON响应
func prettyJSON(json: NSDictionary!) -> String! {
    var pretty: String!

    if json != nil && NSJSONSerialization.isValidJSONObject(json!) {
        if let data = try? NSJSONSerialization.dataWithJSONObject(json!, options: NSJSONWritingOptions.PrettyPrinted) {
            pretty = NSString(data: data, encoding: NSUTF8StringEncoding) as? String
        }
    }
    return pretty
}

清单 12-26 展示了到目前为止我们为 APIClient 库编写的全部代码。

清单12-26. APIClient.swift 库
import Foundation

class APIClient {
    var apiVersion: String!
    var baseURL: String = "http://10.0.1.128:8080"
    var viewController: ViewController!

    required init(parent: ViewController!) {
        viewController = parent
    }

    func blinkAllLights() {
        // GET /blink
        getData(APIService.BLINK)
    }

    func blinkLight(color: String) {
        // GET /blink/red
        getData(APIService.BLINK, id: color)
    }

    func postData(service: APIService, id: String! = nil, urlSuffix: NSArray! = nil, params: [String:String]! = [:]) {
        let blockSelf = self
        let logger: UILogger = viewController.logger

        self.apiRequest(
            service,
            method: APIMethod.POST,
            id: id,
            urlSuffix: urlSuffix,
            inputData: params,
            callback: {(responseJson: NSDictionary!, responseError: NSError!) -> Void in
                if (responseError != nil) {
                    logger.logEvent(responseError!.description)
                    // Handle here the error response in some way
                } else {
                    blockSelf.processPOSTData(service, id: id, urlSuffix: urlSuffix, params: params, responseJson: responseJson)
                }
            })
    }

    func processPOSTData(service: APIService, id: String!, urlSuffix: NSArray!, params: [String:String]! = [:], responseJson: NSDictionary!) {
        // do something with data here
    }

    func getData(service: APIService, id: String! = nil, urlSuffix: NSArray! = nil, params: [String:String]! = [:]) {
        let blockSelf = self
        let logger: UILogger = viewController.logger

        self.apiRequest(
            service,
            method: APIMethod.GET,
            id: id,
            urlSuffix: urlSuffix,
            inputData: params,
            callback: {(responseJson: NSDictionary!, responseError: NSError!) -> Void in
                if (responseError != nil) {
                    logger.logEvent(responseError!.description)
                    // Handle here the error response in some way
                } else {
                    blockSelf.processGETData(service, id: id, urlSuffix: urlSuffix, params: params, responseJson: responseJson)
                }
            })
    }

    func processGETData(service: APIService, id: String!, urlSuffix: NSArray!, params: [String:String]! = [:], responseJson: NSDictionary!) {
        // do something with data here
    }

    func apiRequest(
        service: APIService,
        method: APIMethod,
        id: String!,
        urlSuffix: NSArray!,
        inputData: [String:String]!,
        callback: (responseJson: NSDictionary!, responseError: NSError!) -> Void) {

        // Compose the base URL
        var serviceURL = baseURL + "/"
        if apiVersion != nil {
            serviceURL += apiVersion + "/"
        }
        serviceURL += service.toString()
        if id != nil && !id.isEmpty {
            serviceURL += "/" + id
        }

        let request = NSMutableURLRequest()
        request.HTTPMethod = method.toString()

        // The urlSuffix contains an array of strings that we use to compose the final URL
        if urlSuffix?.count > 0 {
            serviceURL += "/" + urlSuffix.componentsJoinedByString("/")
        }

        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.URL = NSURL(string: serviceURL)

        if !inputData.isEmpty {
            serviceURL += "?" + asURLString(inputData)
            request.URL = NSURL(string: serviceURL)
        }

        // now make the request
        let logger: UILogger = viewController.logger
        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithRequest(request) { (data: NSData?, urlResponse: NSURLResponse?, error: NSError?) -> Void in
            // the request returned with a response or possibly an error
            logger.logEvent("URL: " + serviceURL)
            var error: NSError?
            var jsonResult: NSDictionary?

            if urlResponse != nil {
                let rData: String = NSString(data: data!, encoding: NSUTF8StringEncoding)! as String
                if data != nil {
                    do {
                        try jsonResult = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
                    } catch {
                        // we expect an"OK" from the API, not JSON, so it's OK if we don't do anything here
                    }
                }
                logger.logEvent("RESPONSE RAW: " + (rData.isEmpty ? "No Data" : rData))
                print("RESPONSE RAW: \(rData)")
            } else {
                error = NSError(domain: "response", code: -1, userInfo: ["reason": "blank response"])
            }
            callback(responseJson: jsonResult, responseError: error)
        }
        task.resume()
    }

    func asURLString(inputData: [String:String]! = [:]) -> String {
        var params: [String] = []
        for (key, value) in inputData {
            params.append([key.escapeUrl(), value.escapeUrl()].joinWithSeparator("="))
        }
        params = params.sort { $0 < $1 }
        return params.joinWithSeparator("&")
    }

    func prettyJSON(json: NSDictionary!) -> String! {
        var pretty: String!

        if json != nil && NSJSONSerialization.isValidJSONObject(json!) {
            if let data = try? NSJSONSerialization.dataWithJSONObject(json!, options: NSJSONWritingOptions.PrettyPrinted) {
                pretty = NSString(data: data, encoding: NSUTF8StringEncoding) as? String
            }
        }
        return pretty
    }
}

extension String {
    func escapeUrl() -> String {
        let source: NSString = NSString(string: self)
        let chars = "abcdefghijklmnopqrstuvwxyz"
        let okChars = chars + chars.uppercaseString + "0123456789.~_-"
        let customAllowedSet = NSCharacterSet(charactersInString: okChars)
        return source.stringByAddingPercentEncodingWithAllowedCharacters(customAllowedSet)!
    }
}

enum APIService {
    case BLINK

    func toString() -> String {
        var service: String!

        switch self {
        case .BLINK:
            service = "blink"
        }

        return service
    }
}

enum APIMethod {
    case GET, POST

    func toString() -> String {
        var method: String!

        switch self {
        case .GET:
            method = "GET"
        case .POST:
            method = "POST"
        }

        return method
    }
}

现在您应该能够向设备发送命令,并且您将在文本区域中看到结果,如图 12-4 所示。

示意图3

总结

在本章节中,你学习了如何设置一个基本脚本,以与树莓派上的资源进行交互,运行接收远程命令的监听器所需的服务,以及如何编写一个非常基础的iOS应用程序,通过发起HTTP请求,利用你创建的极简API与设备进行交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值