树莓派安卓项目-全-

树莓派安卓项目(全)

原文:zh.annas-archive.org/md5/82683C8EDBA50EABD87C138CE7CE4264

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

制造者社区中最受欢迎的小工具Raspberry Pi和最受欢迎的智能手机操作系统Android在本书中结合起来,产生了令人兴奋、有用且易于跟随的项目。所涵盖的项目在您日常与 Pi 的互动中非常有用,并且可以作为更令人惊奇的项目的构建模块。

本书涵盖内容

第一章,从任何地方远程连接到 Pi 的远程桌面,教会您如何进行初始设置,以便从世界上任何地方的 Android 设备远程连接到 Pi 桌面。

第二章,使用 Pi 进行服务器管理,在前一章的基础上管理 Pi 和我们在其上安装的不同服务器。我们甚至会介绍一个有趣、有用的项目,利用这些服务器。

第三章,从 Pi 直播监控摄像头,向您展示了如何将 Pi 变成网络摄像头,然后介绍了如何将其用于监控模式的技术,通过 Android 设备和互联网可访问。

第四章,将 Pi 变成媒体中心,向您展示如何将 Pi 变成可从 Android 设备控制的媒体中心。

第五章,使用 Pi 的未接来电,介绍了通过蓝牙从 Android 访问 Pi 上的传感器和组件所需的技术,并展示了 Pi 如何通知您手机上接收到的来电。

第六章,车载 Pi,帮助您将 Pi 连接到您的汽车,并从 Android 手机上进行跟踪。

您需要为本书准备什么

本书中使用的所有软件都可以在互联网上免费获得。您需要 Raspberry Pi 2 和一个 Android 设备。在一些章节中,我们甚至会使用 USB 无线网卡、DHT11 或 DHT22 温度传感器、跳线、LED 灯、USB 蓝牙适配器、Pi 摄像头、USB GPS 接收器和 OBD 蓝牙接口,所有这些都可以在在线商店购买。

本书的读者

Raspberry Pi Android Projects的目标是想要通过 Android 手机控制 Pi 创建引人入胜和有用的项目的读者。不需要先前对 Pi 或 Android 的知识。在每章的末尾,您将成功地创建一个可以日常使用的项目,并将具备能够帮助您将来开发更令人兴奋的项目的技能。本书涵盖的项目将包含一些较小的编程步骤,即使对于最没有经验的读者,这些步骤也将被详细描述。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"下一步是安装一个名为x11vnc的组件。"

一段代码设置如下:

network={
    ssid="THE ID OF THE NETWORK YOU WANT TO CONNECT"
    psk="PASSWORD OF YOUR WIFI"
}

任何命令行输入或输出都以以下方式书写:

sudo apt-get install apache2
sudo apt-get install php5 libapache2-mod-php5
sudo apt-get install libapache2-mod-auth-mysql php5-mysql

新术语重要词汇以粗体显示。您在屏幕上看到的词语,例如菜单或对话框中的词语,会以这样的方式出现在文本中:"通过按下连接按钮来建立连接,现在您应该能够在您的 Android 设备上看到 Pi 桌面了。"

注意

警告或重要提示会以这样的方式出现在一个框中。

提示

提示和技巧会以这样的方式出现。

第一章:从任何地方远程连接到您的 Pi 的远程桌面连接

在这个项目中,我们将对 Pi 和安卓平台进行简单介绍,为我们暖身。当用户希望管理 Pi 时,许多用户都会面临类似的问题。你必须靠近你的 Pi,并连接一个屏幕和一个键盘。我们将通过远程连接到我们的 Pi 桌面界面来解决这个日常问题。本章涵盖以下主题:

  • 先决条件

  • 在您的 Pi 上安装 Linux

  • 在设置中进行必要的更改

  • 在树莓派和安卓中安装必要的组件

  • 连接 Pi 和安卓

先决条件

本章中将使用以下物品,并且需要完成项目:

  • 树莓派 2 型 B 型号:这是树莓派家族的最新成员。它取代了以前的 Pi 1 型 B+。以前的型号应该可以很好地完成本书涵盖的项目的目的。

  • MicroSD 卡:树莓派基金会建议使用 8GB 的 6 级 MicroSD 卡。

  • 安卓设备:设备至少应该有 1.5 或更高版本的安卓系统,这是本章中使用的应用所需的版本。在接下来的一些激动人心的项目中,我们将需要安卓 4.3 或更高版本。

  • HDMI 线:这将用于将 Pi 连接到屏幕进行初始设置。

  • 以太网线:这将用于网络连接。

  • 电脑:这将用于将 Raspbian 操作系统复制到 MicroSD 卡上。

  • USB 鼠标:这将在初始设置期间使用。

以下图片显示了树莓派 2 型 B 型号:

先决条件

树莓派 2 型 B 型号

在您的 Pi 上安装 Linux

我们将在 Pi 上使用Raspbian作为操作系统。我的选择完全基于它是树莓派基金会推荐的事实。它基于 Linux 的 Debian 版本,并针对树莓派硬件进行了优化。除了是树莓派最常用的操作系统外,它还包含了几乎 35000 个软件包,如游戏、邮件服务器、办公套件、互联网浏览器等等。在我写这本书的时候,最新版本是 2015 年 5 月 5 日发布的。

安装 Raspbian 有两种主要方法。你可以使用整个操作系统镜像,也可以从一个名为NOOBS的易于使用的工具-操作系统捆绑包开始。我们将在这里涵盖这两种情况。

注意

有一些带有 NOOBS 或 Raspbian 预装的 SD 卡出售。也许买一个这样的会是个好主意,可以跳过本章的操作系统安装部分。

然而,在我们开始之前,我们可能需要格式化我们的 SD 卡,因为以前的操作系统安装可能会损坏卡。如果你使用计算机操作系统提供的格式化工具格式化了卡,但卡上只显示了一小部分可用空间,你会注意到这一点。我们将使用的工具叫做SD Formatter,可以从SD 协会的网站上下载 Mac 和 Windows 版本,网址是www.sdcard.org/downloads/formatter_4/index.html。安装并启动它。你会看到以下界面,要求你选择 SD 卡位置:

在您的 Pi 上安装 Linux

SD Formatter 界面

使用 NOOBS 安装

最新版本的 NOOBS 可以在downloads.raspberrypi.org/NOOBS_latest找到。下载并解压内容到 SD 卡上。将卡插入 Pi 并使用 HDMI 线连接到屏幕。不要忘记连接 USB 鼠标。当 Pi 连接到电源时,你将看到一个你可以选择的列表。在列表上选择Raspbian安装选项,然后点击安装。这将在您的 SD 卡上安装 Raspbian 并重新启动 Pi。

使用 Raspbian 镜像安装

Raspbian OS 的最新版本可以在downloads.raspberrypi.org/raspbian_latest找到。ZIP 文件大小接近 1GB,包含一个扩展名为.img的单个文件,大小为 3.2GB。解压内容并按照下一节中的步骤将其提取到合适的 microSD 卡中。

将 OS 映像提取到 SD 卡

要提取映像文件,我们需要一个磁盘映像实用程序,我们将在 Windows 上使用一个名为Win32 Disk Imager的免费可用实用程序。它可以在sourceforge.net/projects/win32diskimager/上下载。在 Mac OS 上,有一个类似的工具叫做ApplePi Baker,可以在www.tweaking4all.com/hardware/raspberry-pi/macosx-apple-pi-baker/上找到。下载并安装到您的计算机上。安装将包含一个可执行文件Win32DiskImager,您应该右键单击它并选择以管理员身份运行

Win32 Disk Imager窗口中,您应该选择您提取的映像文件和 SD 卡驱动器,类似于以下截图所示:

将 OS 映像提取到 SD 卡

Win32 Disk Imager 窗口

单击Write按钮将启动该过程,您的 SD 卡将准备好插入 Pi 中。

在设置中进行必要的更改

当 Pi 仍然连接到带有 HDMI 的屏幕时,使用以太网连接到网络。当 Pi 第一次启动时,您将看到一个设置实用程序,如下截图所示:

在设置中进行必要的更改

树莓派软件配置工具

您还可以选择列表中的第一个选项扩展文件系统。同时选择第三个选项启用启动到桌面

在下一个菜单中,选择列表中的第二项以用户'pi'身份在图形桌面登录。然后,选择并选择Yes以重新启动设备。

在设置中进行必要的更改

在配置工具中选择桌面启动

重新启动后,您将看到 Raspbian 的默认桌面管理器环境LXDE

在 Pi 和 Android 中安装必要的组件

如下截图所示,LXDE 桌面管理器带有初始设置和一些预装程序:

在 Pi 和 Android 中安装必要的组件

LXDE 桌面管理环境

通过点击位于顶部的选项卡栏上的屏幕图像,您将能够打开一个终端屏幕,我们将用它来向 Pi 发送命令。

下一步是安装一个名为x11vnc的组件。这是 Linux 的窗口管理组件 X 的 VNC 服务器。在终端上输入以下命令:

sudo apt-get install x11vnc

这将下载并安装x11vnc到 Pi。我们甚至可以设置一个密码,供 VNC 客户端远程桌面连接到这个 Pi 时使用以下命令并提供稍后使用的密码:

x11vnc –storepasswd

接下来,我们可以在 Pi 重新启动并启动 LXDE 桌面管理器时运行x11vnc服务器。可以通过以下步骤完成:

  1. 进入位于/home/pi的 Pi 用户的.config目录:
cd /home/pi/.config

  1. 在这里创建一个名为autostart的子目录:
mkdir autostart

  1. 进入autostart目录:
cd autostart

  1. 开始编辑一个名为x11vnc.desktop的文件。作为终端编辑器,我正在使用nano,这是树莓派上新手用户最容易使用的编辑器,但也有更令人兴奋的替代方案,比如vi
nano x11vnc.desktop

将以下内容添加到此文件中:

[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=X11VNC
Comment=
Exec=x11vnc -forever -usepw -display :0 -ultrafilexfer
StartupNotify=false
Terminal=false
Hidden=false
  1. 如果您使用nano作为您选择的编辑器,请使用(Ctrl+X, Y, Enter)保存并退出。

  2. 现在,您应该重新启动 Pi,使用以下命令来运行服务器:

sudo reboot

使用sudo reboot命令重新启动后,我们现在可以通过发出ifconfig命令在终端窗口中找出树莓派被分配的 IP 地址。分配给您的树莓派的 IP 地址可以在eth0条目下找到,并且在inet addr关键字之后给出。记下这个地址:

在树莓派和 Android 上安装必要组件

ifconfig 命令的示例输出

  1. 下一步是在您的 Android 设备上下载 VNC 客户端。

在本项目中,我们将使用一个名为androidVNC的免费可用的 Android 客户端,或者在 Play 商店中称为androidVNC 团队+antlersoftVNC Viewer for Android。在撰写本书时使用的最新版本是 0.5.0。

注意

请注意,为了能够将您的 Android VNC 客户端连接到树莓派,树莓派和 Android 设备都应该连接到同一网络——Android 通过 Wi-Fi 连接,树莓派通过其以太网端口连接。

连接树莓派和 Android

在您的设备上安装并打开 androidVNC。您将看到一个要求连接详细信息的第一个活动用户界面。在这里,您应该提供连接的昵称,您在运行x11vnc –storepasswd命令时输入的密码,以及使用ifconfig命令找到的树莓派的 IP 地址。通过按下连接按钮启动连接,您现在应该能够在您的 Android 设备上看到树莓派的桌面。

在 androidVNC 中,您应该能够通过点击屏幕来移动鼠标指针,并且在 androidVNC 应用程序的选项菜单下,您将找到如何使用EnterBackspace向树莓派发送文本和按键的方法。

注意

您甚至可能会发现从另一台计算机连接到树莓派很方便。我建议在 Windows、Linux 和 Mac OS 上使用 RealVNC 来实现这一目的。

如果我想在树莓派上使用 Wi-Fi 呢?

为了在树莓派上使用 Wi-Fi dongle,首先,使用以下命令打开nano编辑器打开wpa_supplicant配置文件:

sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

将以下内容添加到此文件的末尾:

network={
    ssid="THE ID OF THE NETWORK YOU WANT TO CONNECT"
    psk="PASSWORD OF YOUR WIFI"
}

注意

我假设您已经设置了无线家庭网络以使用 WPA-PSK 作为认证机制。如果您有其他机制,您应该参考wpa_supplicant文档。LXDE 提供了更好的通过 GUI 连接到 Wi-Fi 网络的方法。它可以在树莓派桌面环境的右上角找到。

从任何地方连接

现在,我们已经从我们的设备连接到了树莓派,我们需要连接到与树莓派相同的网络。但是,我们大多数人也希望能够从世界各地连接到树莓派。为了做到这一点,首先,我们需要知道由网络提供商分配给我们的家庭网络的 IP 地址。通过访问whatismyipaddress.com网址,我们可以找出我们家庭网络的 IP 地址。下一步是登录到我们的路由器并打开来自世界各地对树莓派的请求。为此,我们将使用大多数现代路由器上找到的一个名为端口转发的功能。

注意

要注意端口转发中包含的风险。您正在向全世界开放对树莓派的访问权限,甚至对恶意用户也是如此。我强烈建议在执行此步骤之前更改用户pi的默认密码。您可以使用passwd命令更改密码。

通过登录到路由器的管理门户并导航到端口转发选项卡,我们可以打开对树莓派内部网络 IP 地址的请求,这是我们之前找到的,并且 VNC 服务器的默认端口是5900。现在,我们可以在世界各地提供我们的外部 IP 地址给 androidVNC,而不是仅在我们与树莓派在同一网络上时才能使用的内部 IP 地址。

从任何地方连接

Netgear 路由器管理页面上的端口转发设置

注意

请参考您路由器的用户手册,了解如何更改端口转发设置。大多数路由器要求您通过以太网端口连接以访问管理门户,而不是通过 Wi-Fi。

动态局域网 IP 地址和外部 IP 地址的问题

这个设置有一个小问题。树莓派可能在每次重新启动时获得一个新的局域网 IP 地址,使得端口转发设置变得无用。为了避免这种情况,大多数路由器提供了地址保留设置。你可以告诉大多数路由器,每当连接一个具有唯一 MAC 地址的设备时,它应该获得相同的 IP 地址。

另一个问题是,您的互联网服务提供商ISP)可能会在每次重新启动路由器或出于其他原因为您分配新的 IP 地址。您可以使用动态 DNS 服务,如 DynDNS,来避免这些问题。大多数路由器都能够使用动态 DNS 服务。或者,您可以通过联系您的 ISP 来获得一个静态 IP 地址。

摘要

在这个项目中,我们安装了 Raspbian,启动了树莓派,启用了桌面环境,并使用安卓设备连接到了树莓派。

在下一章中,我们将直接访问树莓派的控制台,甚至可以使用我们的安卓设备通过 FTP 传输文件到树莓派和从树莓派中传输文件。

第二章:使用 Pi 进行服务器管理

在这个项目的前半部分,我们将从基于桌面的控制台转移到一个基于文本的控制台,这样用户就可以获得更多的权力,并且可以执行比桌面更高级的任务。我们将从 Android 设备访问 Pi 的 Linux 控制台并远程控制它。在后半部分,我们将通过 FTP 在 Pi 和 Android 之间发送和接收文件。我们甚至将通过使用基于文本的控制台远程管理我们新安装的 FTP 服务器来结合这两部分。在本章中,我们甚至将在 Pi 上安装数据库和 Web 服务器,以展示如何以后管理它们。为了使它更有趣,我们将实现一个简单但有用的迷你项目,利用 Web 和数据库服务器。以下主题将被涵盖:

  • 从 Android 远程控制台到 Pi

  • 在 Pi 和 Android 之间交换文件

  • 一个简单的数据库和 Web 服务器实现

  • 服务器的简单管理

从 Android 远程控制台到 Pi

Linux 和 Unix 计算机的管理员多年来一直在使用称为shell的基于文本的命令行界面来管理和管理他们的服务器。由于 Pi 的操作系统 Raspbian 是 Linux 变种,因此访问并发出命令或检查 Pi 上运行的程序、服务和不同服务器的状态的最自然方式是再次通过在基于文本的 shell 上发出命令。有不同的 shell 实现,但在 Raspbian 上默认使用的是bash。在 Linux 服务器上远程访问 shell 的最著名方式是通过一般称为SSH安全外壳协议。

注意

安全外壳SSH)是一种加密的网络协议,用于以安全的方式向远程计算机发送外壳命令。SSH 为您做了两件事。它通过不同的工具,比如我们马上会向您介绍的工具,使您能够通过安全通道在不安全的网络上向远程计算机发送命令。

为了使 SSH 工作,应该已经有一个可以接受并响应 SSH 客户端请求的 SSH 服务器正在运行。在树莓派上,默认情况下启用了此功能。如果以任何方式禁用了它,您可以使用 Pi 配置程序通过发出以下命令来启用它:

sudo raspi-config

然后,导航到ssh并按Enter,然后选择启用或禁用 ssh 服务器

在客户端,由于我们在本书中一直在使用 Android 作为我们的客户端,我们将下载一个名为 ConnectBot 的应用程序。这是 Android 上最受欢迎的 SSH 客户端之一,截至今天的最新版本是 1.8.4。将其下载到您的设备并打开它。

您需要提供我们在上一章中找到的用户名和 IP 地址。在这种情况下,我们不需要提供端口,因为 ConnectBot 将使用 SSH 的默认端口。如果由于主机的真实性问题而要求继续连接,请单击。您会被问到这个问题,因为您是通过远程 SSH 首次连接到 Pi。

请注意,在以下屏幕截图中,我提供了我家庭网络的内部 IP 地址。您可能希望使用外部 IP 地址并从家庭网络外部连接到 Pi。为此,您还需要将标准 FTP 端口2120添加到端口转发设置中。SSH 协议也适用,其默认端口号为22

注意

正如我们之前讨论过的,以这种方式打开端口存在安全风险,同时保留 Pi 用户pi的默认密码也存在安全风险。

以下屏幕截图显示了 ConnectBot 上的连接详细信息:

从 Android 远程控制台到 Pi

ConnectBot 上的连接详细信息

现在,提供pi账户的默认密码,即raspberry,或者您已经更改的密码。完成此步骤后,您将可以使用 SSH 远程连接到 Pi,如下截图所示:

从 Android 到 Pi 的远程控制台

ConnectBot 提供的提示

您现在可以准备在 Pi 上发出命令并检查不同服务的状态。此连接将保存其所有属性。下次您想要登录时,将无需提供地址、用户名和密码信息。

注意

在 Mac 或 Linux 上,您可以使用系统默认安装的ssh命令。在 Windows 上,您可以下载 PuTTY 来发出与 ConnectBot 中相同的命令。

在 Pi 和 Android 之间交换文件

在本章的第二部分中,我们将使用 Pi 作为 FTP 服务器,在我们的 Android 设备之间共享文件,或者将文件发送到 Pi 上,以便在连接到 Pi HDMI 端口的大屏幕上查看。我们将使用的 FTP 服务器是vsftpd。它是许多小型项目中使用的轻量级 FTP 服务器。要在我们的 Pi 上安装它,我们使用以下命令:

sudo apt-get install vsftpd

上述命令甚至会启动 FTP 服务。

但是,我们应该在 FTP 服务器的配置中进行一些更改,以有效地使用它。为此,我们需要使用以下命令编辑 FTP 服务器配置文件:

sudo nano /etc/vsftpd.conf

找到包含#local_enable=YES#write_enable=YES的两行,并在保存并退出之前删除这些行开头的#注释符号。这些更改将使用户pi能够登录并能够将文件发送到 Pi。要重新启动 FTP 服务器,请发出此命令:

sudo service vsftpd restart

现在,我们需要在 Android 上安装一个 FTP 客户端。为此,我们将使用AndFTP。对于我们的项目来说,使用免费版本就足够了。在打开后,我们在 Android 设备上看到以下初始视图:

在 Pi 和 Android 之间交换文件

AndFTP 客户端的初始视图

按下加号按钮将带您到以下视图,您将被要求提供连接属性:

在 Pi 和 Android 之间交换文件

AndFTP 上的连接属性

提供您在第一章中找到的 Pi 的 IP 地址,用户名为pi,密码为raspberry或您已更改的密码。然后,向下滚动到视图的末尾,然后按下保存按钮。这将保存连接属性并将您带回到主视图:

在 Pi 和 Android 之间交换文件

AndFTP 中的连接列表

单击新创建的连接,显示为蓝色文件夹,将启动到 Pi 的 FTP 连接并登录用户pi。这将使您进入pi用户的home目录,如下截图所示:

在 Pi 和 Android 之间交换文件

用户 pi 的主目录

现在,您将能够通过在 AndFTP 中按下类似手机的图标并选择要上传的文件来从您的 Android 设备上传文件到 Pi。您可以使用同一网络上的另一台 Android 设备或甚至使用内置的 FTP 客户端在另一台计算机上设置 AndFTP,并下载新上传的文件以查看它;这样,您已经使用树莓派作为 FTP 服务器在不同的 Android 客户端之间共享了第一个文件。

一个简单的数据库和 Web 服务器实现

接下来,我们将进一步进行我们的项目,并安装数据库和 Web 服务器,稍后我们可以使用 ConnectBot 进行管理。我们甚至会更有趣,通过实施一个真实的项目来使用这些服务器。这个目的的最佳候选者是传感器测量场景。我们将把一个温度/湿度传感器连接到我们的树莓派,并将测量值保存到我们将在树莓派上安装的数据库中,一个 Web 服务器将使客户端可以访问。我们以后可以远程管理这些服务器,这是本章的主要目标。

连接传感器

为了这个项目的目的,我们将使用一个传感器DHT11,它可以测量温度和湿度,但为了更容易连接,我们将使用一个现成的模块称为Keyes DHT11或简称 DHT11,其中包含了这些传感器。

提示

甚至还有一个改进版的 DHT11,叫做 DHT22。它的成本略高,但传感器更精确。

使用这个传感器模块而不是传感器本身将使我们能够只使用三根跳线将传感器连接到树莓派,而无需面包板或电阻。使用这个模块而不是传感器的另一个优点是:传感器提供的是树莓派无法处理的模拟数据。树莓派能够处理其 GPIO 端口上的数字信息。DHT11 模块为我们进行了转换。以下图片说明了 DHT11 传感器模块以及与之相关的引脚的描述:

连接传感器

DHT11 传感器模块

以下图片说明了 Keyes DHT11 传感器模块:

连接传感器

Keyes DHT11 传感器模块

现在,将传感器模块的GND输出连接到树莓派的 GPIO 接地,5V输出连接到树莓派的 5V 引脚,DATA连接到树莓派的GPIO-4引脚。以下图示了树莓派 GPIO 引脚的布局及其名称:

连接传感器

树莓派 GPIO 引脚布局

下一步是读取这些传感器提供的值。为此,我们将使用Adafruit的一个广泛使用的库,这个库是专门为 Python 编程语言开发的这类传感器而设计的。在我们使用它之前,我们需要将一些软件组件安装到我们的树莓派上。

首先,我们需要使用以下命令更新我们的树莓派并安装一些依赖项:

sudo apt-get update
sudo apt-get install build-essential python-dev

传感器库本身在 GitHub 上,我们将使用以下命令从 GitHub 上下载到我们的树莓派上:

git clone https://github.com/adafruit/Adafruit_Python_DHT.git

这个命令会下载库并将其保存在一个子目录中。现在,使用以下命令进入这个子目录:

cd Adafruit_Python_DHT

接下来,您需要使用以下命令实际安装传感器库:

sudo python setup.py install

在这里,我们使用了标准的 Python 第三方模块安装功能,它会将 Adafruit 库全局安装到您的系统的标准 Python 库安装位置/usr/local/lib/python2.7/dist-packages/。这就是为什么我们需要超级用户权限,我们可以使用sudo命令来获得。

现在我们准备开始使用我们下载的示例代码从传感器中读取测量值。假设您仍然在Adafruit_Python_DHT目录中,以下命令可以完成任务:

sudo ./examples/AdafruitDHT.py 11 4

在这个命令中,11是用来识别 DHT11 传感器的描述符,4表示 GPIO 引脚 4。您现在应该得到一个看起来像这样的输出:

Temp=25.0*C  Humidity=36.0%

安装数据库

在验证传感器和连接到树莓派的连接工作正常后,我们将把测量值保存到数据库中。我们将使用的数据库是 MySQL。使用以下命令将 MySQL 安装到树莓派上:

sudo apt-get install mysql-server python-mysqldb

在安装过程中,您将被要求为管理员帐户 root 设置密码。我将 admin 设置为密码,并在即将到来的代码中引用它。以下命令将带您进入 MySQL shell,在那里您可以发出 SQL 命令,例如将数据插入数据库或查询已在数据库中的数据。当要求时,您应提供您设置的密码:

mysql -u root -p

您可以随时使用exit命令退出 MySQL shell。

在 MySQL shell 中的下一步是创建一个数据库,并将其用于随后的任何 SQL 语句:

mysql> CREATE DATABASE measurements;
mysql> USE measurements;

以下 SQL 语句将在这个新创建的数据库中创建一个表,我们将用它来保存传感器测量值:

mysql> CREATE TABLE measurements (ttime DATETIME, temperature NUMERIC(4,1), humidity NUMERIC(4,1));

下一步是实现一个 Python 脚本,该脚本从我们的传感器中读取并将其保存到数据库中。使用先前讨论的nano命令将以下代码放入名为sense.py的文件中,该文件位于home目录下。您可以使用没有参数的cd命令从pi目录结构中的任何位置返回到home目录。请注意一个重要的事实,即文件不应包含任何空的前导行,这意味着引用 Python 命令的行应该是文件中的第一行。以下代码构成了我们的sense.py文件的内容:

#!/usr/bin/python

import sys
import Adafruit_DHT
import MySQLdb

humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, 4)
#temperature = temperature * 1.8 + 32 # fahrenheit
print str(temperature) + " " + str(humidity)
if humidity is not None and temperature is not None:
    db = MySQLdb.connect("localhost", "root", "admin", "measurements")
    curs = db.cursor()
    try:
        sqlline = "insert into measurements values(NOW(), {0:0.1f}, {1:0.1f});".format(temperature, humidity)
        curs.execute(sqlline)
        curs.execute ("DELETE FROM measurements WHERE ttime < NOW() - INTERVAL 180 DAY;")
        db.commit()
        print "Data committed"
    except MySQLdb.Error as e:
        print "Error: the database is being rolled back" + str(e)
        db.rollback()
else:
    print "Failed to get reading. Try again!"

注意

您应该将MySQLdb.connect方法调用中的密码参数更改为您在 MySQL 服务器上为 root 用户分配的密码。出于安全原因,您甚至应考虑创建一个仅访问measurements表的新用户,因为 root 用户对数据库具有完全访问权限。有关此目的,请参阅 MySQL 文档。

下一步是更改文件属性,并使用以下命令将其设置为可执行文件:

chmod +x sense.py

请注意,此脚本仅保存单个测量值。我们需要安排运行此脚本。为此,我们将使用一个名为cron的内置 Linux 实用程序,它允许 cron 守护程序在后台定期运行任务。crontab,也称为 CRON TABle,是一个包含要在指定时间运行的 cron 条目的时间表文件。通过运行以下命令,我们可以编辑此表:

crontab –e

将以下行添加到此文件中并保存。这将使 cron 守护程序每五分钟运行一次我们的脚本:

*/5 * * * * sudo /home/pi/sense.py

安装 Web 服务器

现在,我们将测量结果保存到数据库中。下一步是使用 Web 服务器在 Web 浏览器中查看它们。为此,我们将使用Apache作为 Web 服务器,PHP作为编程语言。要安装 Apache 和我们的目的所需的软件包,请运行以下命令:

sudo apt-get install apache2
sudo apt-get install php5 libapache2-mod-php5
sudo apt-get install libapache2-mod-auth-mysql php5-mysql

然后,将目录更改为 Web 服务器的默认目录:

cd /var/www

在这里,我们将创建一个文件,用户可以通过我们安装的 Web 服务器访问该文件。该文件由 Web 服务器执行,并将执行结果发送给连接的客户端。我们将其命名为index.php

sudo nano index.php

内容应如下所示。在这里,您应该再次更改 MySQL 用户 root 的密码,以与对new mysqli构造方法的调用中选择的密码相匹配:

<?php

// Create connection
$conn = new mysqli("localhost", "root", "admin", "measurements");
// Check connection
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

$sql = "SELECT ttime, temperature, humidity FROM measurements WHERE ttime > NOW() - INTERVAL 3 DAY;";
$result = $conn->query($sql);
?>
<html>
<head>
<!-- Load c3.css -->
<link href="https://rawgit.com/masayuki0812/c3/master/c3.min.css" rel="stylesheet" type="text/css">

<!-- Load d3.js and c3.js -->
<script src="img/d3.min.js" charset="utf-8"></script>
<script src="img/c3.min.js"></scrip>
</head>
<body>
<div id="chart"></div>

<script>

<?php

if($result->num_rows > 0) {
?>
var json = [
<?php
  while($row = $result->fetch_assoc()) {
    ?>{ttime:'<?=$row["ttime"]?>',temperature:<?=$row["temperature"]?> ,humidity:<?=$row["humidity"]?>},<?
  }
}
?>
];
<?php
$conn->close();
?>
var chart = c3.generate({
    bindto: '#chart',
    data: {
      x: 'ttime',
      xFormat: '%Y-%m-%d %H:%M:%S', 
      keys: {
        x:'ttime',
        value: ['temperature', 'humidity']
      },
      json: json,
      axes: {
        temperature: 'y',
        humidity: 'y2'
      }
    },
    axis: {
        x: {
            type: 'timeseries',
            tick: {
                format: '%Y-%m-%d %H:%M'
            }
        },
        y: {
            label: 'temperature'
        },
        y2: {
            show: true,
            label: 'humidity'
        }
    }
});
</script>
</body>
</html>

我们希望此页面成为 Web 浏览器直接访问服务器时的默认起始页面。您可以按以下方式备份 Apache 的旧默认起始页面:

sudo mv index.html oldindex.html

在浏览器中导航到 Pi 的 IP 地址将在几个小时的传感器测量后产生类似以下截图的视图。在这里,我可以使用家庭网络外部的外部 IP 地址访问 Pi,因为我已将家庭路由器的端口转发设置添加到了80的 HTTP 端口。

安装 Web 服务器

现在,我们有一个正在运行的 FTP、数据库和 Web 服务器。让我们使用 ConnectBot 进行管理。

服务器的简单管理

以下命令仅检查 FTP 服务器的状态:

service vsftpd status

如果 FTP 服务器出现问题,可以使用此命令重新启动:

sudo service vstfpd restart

我们使用的service实用程序允许您使用以下两个命令重新启动数据库和 Web 服务器:

sudo service mysql restart
sudo service apache2 restart

使用以下命令检查 MySQL 服务器的状态:

mysqladmin -u root -p status

如果您认为数据库的大小已经增长太大,可以启动 MySQL 控制台并运行 SQL 查询以查看数据库大小:

mysql –u root –p
mysql> SELECT table_schema "DB", Round(Sum(data_length + index_length) / 1024 / 1024, 1) "Size in MB" 
FROM   information_schema.tables 
GROUP  BY table_schema;

甚至可以使用以下查询删除三天前的记录:

select measurements;
delete from measurements where ttime < NOW() - INTERVAL 3 DAY;

或者,作为替代方案,您可以使用 shell 命令检查文件系统的大小:

df -h

总结

本章向您介绍了将树莓派作为服务器的管理以及如何从 Android 向其发出命令。我们在树莓派上安装了 FTP 服务器,并在 Android 客户端之间共享文件。为了展示数据库和 Web 服务器的示例,我们实施了一个有用的项目,并学会了远程管理这些服务器。

下一章将向您介绍树莓派摄像头,并帮助您实现监控解决方案。

第三章:从树莓派实时流式传输监控摄像头

在本章中,我们将连接摄像头到树莓派,并从中实时流式传输视频。然后我们将能够从我们的 Android 设备观看这些内容的流式传输。这一章将使我们更接近使用树莓派,远离树莓派的管理。

在本章中,我们将涵盖以下主题:

  • 硬件和软件配置

  • 将视频流式传输到 Android 设备

  • 监控模式

硬件和软件配置

我们将使用为树莓派开发的标准摄像头,在许多主要电子商店的价格约为 30 美元。

硬件和软件配置

树莓派摄像头

树莓派有一个摄像头端口,您可以插入摄像头电缆。树莓派上的插头可以通过向上拉开来打开。如果您在连接摄像头到树莓派时遇到问题,您可以在互联网上找到许多视频来展示如何操作。您可以观看树莓派基金会的视频www.raspberrypi.org/help/camera-module-setup/

下一步是配置树莓派并启用摄像头硬件。这是通过发出以下命令访问的树莓派配置程序完成的:

sudo raspi-config

在提供的菜单中,选择启用摄像头并点击Enter。然后点击完成,您将被提示重新启动。

将视频流式传输到 Android 设备

从树莓派到 Android 的最简单的流式传输方式是使用RaspiCam Remote应用程序,该应用程序登录到树莓派并执行必要的命令。然后自动从树莓派获取流。下载并打开应用程序,在初始视图中提供登录详细信息,如 IP 地址、用户名和密码。请注意,默认情况下,它使用默认的登录帐户详细信息和 SSH 端口。如果您使用默认安装,则只需要 IP 地址。如果您启用端口22的端口转发,甚至可以通过互联网访问摄像头,如第一章, 从任何地方连接到树莓派的远程桌面连接中所述。以下屏幕截图显示了 RaspiCam Remote 应用程序的登录设置:

将视频流式传输到 Android 设备

RaspiCam Remote 应用程序的初始视图

等待几秒钟后,您应该在 Android 设备上看到树莓派摄像头拍摄的第一张照片。点击相机图标后,摄像头将开始流式传输,如下面的屏幕截图所示:

将视频流式传输到 Android 设备

从树莓派流式传输

下一步是使用H.264设置获得更好的流式传输质量。连接到 RaspiCam Remote 应用程序后,您应该打开设置并勾选H.264复选框。但是,在再次通过应用程序连接之前,我们需要使用以下命令在树莓派上安装 VLC 服务器。您可能会时不时地遇到install命令的问题,但再次运行它几乎总是可以解决问题:

sudo apt-get install vlc

下一步是在 Android 上安装更好的 VLC 客户端。我们将使用VLC for Android beta应用程序。安装后,再次打开 RaspiCam Remote 应用程序,然后通过单击相机图标开始流式传输。此时,Android 将要求您选择标准视频播放器或新安装的 VLC for Android beta。选择后者,您将体验到更好的流式传输质量。不要忘记在路由器的端口转发设置中添加端口8080,以便通过互联网访问流式视频。

手动 VLC 配置

RaspiCam 远程应用程序在流视频内容之前会自动配置 Pi 上的 VLC。你们中的一些人可能想要直接从 VLC 应用程序连接到视频流,并跳过 Android 上的 RaspiCam。以下命令与你在 Android 设备上使用 RaspiCam 开始流媒体之前提供的命令相同:

/opt/vc/bin/raspivid -o - -n -t 0 -fps 25 -rot 0 -w 640 -h 480 | /usr/bin/vlc -I dummy stream:///dev/stdin --sout '#standard{access=http,mux=ts,dst=:8080}' :demux=h264 &

如果你发出上述命令,你将能够从 VLC 应用程序查看流媒体内容。你可以通过点击 VLC 应用程序操作栏上的天线图标来建立连接。它将提示输入流地址,即 http://PI_IP_ADDRESS:8080

监视模式

看到摄像头的视频流很酷,但能够在监视模式下运行它更有用。我们希望摄像头能够对运动做出反应,并在检测到运动时保存图像或视频,这样我们可以稍后查看它们,而不是一直盯着视频。为此,我们将开始在我们的 Pi 上安装一个运动识别软件,这个软件因为明显的原因被称为 motion

sudo apt-get install motion

这将安装 motion 软件,以下命令将向内核添加必要的软件包:

sudo modprobe bcm2835-v4l2

最好将其放在 /etc/rc.local 文件中,以便在启动时运行。不过,你应该将它放在 exit 0 行之前。

我们甚至会进行一些配置更改,以便能够访问 motion 提供的流媒体和控制功能。使用以下命令编辑 motion 的配置文件:

sudo nano /etc/motion/motion.conf

默认情况下,对 motion 的 Web 访问受到限制,只能从本地主机访问,这意味着你不能从 Pi 以外的其他计算机访问它。我们将通过找到 motion.conf 文件中的以下行来更改这种行为:

webcam_localhost on
control_localhost on

请注意,这些不是文件中的连续行。另外,如果你使用 nano 作为你的编辑器,你可以按 Ctrl+W 进入搜索模式。

我们将通过分别用以下代码替换前面的代码来关闭本地主机访问行为:

webcam_localhost off
control_localhost off

此外,我们希望 motion 服务在后台模式下执行,同时作为 daemon 运行。为此,我们应该在同一文件中找到以下代码行:

daemon on

我们应该用这行替换它:

daemon off

如果我们现在启动 motion,我们将得到以下错误:

Exit motion, cannot create process id file (pid file) /var/run/motion/motion.pid: No such file or directory

为了摆脱这个错误,我们可以创建 motion 抱怨的这个文件夹:

sudo mkdir /var/run/motion

请注意,这个目录可能会在启动时被删除,所以最好将这个命令添加到 /etc/rc.local 文件中。

现在,你可以最终启动和停止你的 Pi 摄像头进入监视模式,发出以下命令,最好使用 ConnectBot 应用程序或我们在上一章中讨论过的任何其他 SSH 客户端。以下命令将启动 motion

sudo motion

停止运动,发出以下命令:

sudo pkill -f motion

如果你总是想在启动时运行它,我不建议这样做,因为你的 Pi 可能会存储空间不足,你应该使用以下命令编辑 /etc/default/motion 文件:

sudo nano /etc/default/motion

在这个文件中,你会找到以下行:

start_motion_daemon=no

你应该用这个替换它:

start_motion_daemon=yes

你可以使用以下命令启动服务,或者重新启动你的 Pi,这将自动启动服务:

sudo service motion start

要检查所有服务以及 motion 服务的状态,你可以使用以下命令:

sudo service --status-all

Motion 软件分为两部分。第一部分是您可以观看流视频的地方,第二部分是您可以在检测到运动时查看图像/视频文件的地方。您可以通过打开http://IP_ADRESS_OF_THE_PI:8081网页来查看 motion 软件的流。由于某种原因,motion 软件的这一部分只能在 Firefox 中工作,但是下一节讨论的监视部分将在其他浏览器中工作。请注意,您不能同时启动 motion 服务器和 VLC 通过 RaspiCam 应用程序,因为它们使用相同的端口。以下屏幕截图显示了 motion 视频的流:

监视模式

端口 8081 上的 motion 流视频

您可以使用AndFTP登录到 Pi,如前一章所述,并导航到/tmp/motion文件夹,以查看每当检测到运动时保存的图像。重新启动 motion 服务将清空文件夹的内容。

提示

在路由器的端口转发设置中添加端口80808081和 FTP 端口21,以便从网络外部访问这些服务。

在 Web 上访问监视图像

在几乎所有涉及监视的场景中,我们希望通过互联网访问保存的图像,这些图像是在检测到运动时保存的。为了做到这一点,我们将连接motion保存图像的目录到我们在上一章中已经安装的 Apache 服务器上。运行以下命令将实现这一点:

sudo ln -s /tmp/motion /var/www/motion

您还应该在motion.conf文件中添加motion保存图像和视频的目录,使用以下行:

target_dir /tmp/motion

现在,在浏览器中打开http://IP_ADRESS_OF_THE_PI/motion链接,您将看到motion在摄像头前检测到运动时保存的图像列表。

请注意,如果motion尚未检测到任何运动并创建/tmp/motion/目录,则您可能会从 Web 浏览器中收到访问被禁止的错误。以下屏幕截图说明了 motion 保存的图像列表:

在 Web 上访问监视图像

通过 Web 访问检测到运动时的图像和视频文件

摘要

我们已经从对 Pi 的管理转移到更真实的项目,并在 Pi 上安装了摄像头;因此,可以在 Android 设备和 Web 上查看 Pi 的流。我们甚至学会了如何将 Pi 用作监视摄像头,并查看其检测到的运动。

在下一章中,我们将继续在更有趣的场景中使用 Pi,并将其变成一个媒体中心。

第四章:将您的 Pi 变成媒体中心

在前几章中,我们一直在管理我们的 Pi 并实施有用的项目。在本章中,我们将更多地将我们的 Pi 用作娱乐来源,并将其变成媒体中心。涵盖的主题如下:

  • 在 Pi 上安装和设置媒体中心

  • 通过 Android 远程控制连接到媒体中心

  • 从您的媒体中心获取更多

  • 使用 NOOBS 安装媒体中心

在 Pi 上安装和设置媒体中心

我们选择用于此项目的媒体中心软件是Kodi,以前被称为 XBMC。它是开源的,被广泛使用,并有很多附加组件。

像往常一样,我们将使用apt-get命令在我们的 Pi 上安装必要的软件:

sudo apt-get install kodi

然后,我们将运行kodi-standalone可执行文件,这将启动 Kodi 并在 Pi 的 HDMI 端口上显示其用户界面。因此,重要的是您连接 Pi 到屏幕上使用 HDMI 端口,而不是远程桌面来查看 Kodi 的用户界面。现在,您可以连接 USB 键盘或鼠标来在 Kodi 内进行导航。

启动时启动 Kodi

我们绝对不希望运行一个命令来启动媒体中心,无论从 Android 运行命令有多容易,正如前几章中所讨论的。因此,我们需要使用crontab -e命令在启动时启动命令。在crontab命令打开的文件的末尾添加以下行:

@reboot /usr/bin/kodi-standalone &

现在,每当您重新启动 Pi 时,Kodi 都将自动重新启动。请注意,在这里,您通过 Pi 的 HDMI 端口访问媒体中心,但您也可以使用第一章中讨论的工具通过远程桌面访问。

通过 Android 远程控制连接到媒体中心

当前设置的主要问题是您只能使用连接的键盘或鼠标来控制媒体中心,这使得它不像媒体中心应该那样舒适。但是,有一个名为Kore的 Android 上的 Kodi 远程控制,使得远程控制媒体中心变得非常容易。您可以从 Google Play 下载它。它的官方名称是Kore, Kodi 的官方遥控器,由XBMC Foundation发布,这是一个运营 Kodi 媒体中心项目的非营利组织。

在您可以将 Android 上的远程控制应用连接到 Pi 上的 Kodi 之前,您需要在 Kodi 上进行一些设置更改。转到 Kodi 中的SYSTEM菜单,然后SettingsServicesWebserver。在这里,您应该选择允许通过 HTTP 控制 Kodi。然后转到同一菜单中的Remote control设置,并启用允许此系统上的程序控制 XBMC允许其他系统上的程序控制 XBMC设置。现在在 Android 上打开 Kore 并让它搜索媒体中心。如果手机和媒体中心在同一网络上,Kore 应该能够找到它。搜索成功后,您将看到类似以下截图的视图:

提示

请注意,Kodi 的默认 HTTP 端口与上一章中看到的 motion 服务器的默认 HTTP 端口冲突。您应该在 Kodi 中更改端口设置,或者在更改 Kodi 设置之前停止 motion 服务器。

通过 Android 远程控制连接到媒体中心

Kore 已找到媒体中心

现在,单击新发现的媒体中心以连接并开始远程控制。如果它无法自动识别媒体中心,您可以按Next按钮并手动输入参数。端口8080是默认端口,如果您没有在 Kodi 内更改这些参数,则应使用默认用户名kodi

通过 Android 远程控制连接到媒体中心

Kore 中的手动设置

充分利用你的媒体中心

媒体中心可以用于许多事情。例如,你可以下载插件,让你访问大量的在线内容,如 YouTube、可汗学院和 TED 演讲。

在 Android 设备上使用 Kodi 观看视频

另一件有趣的事情是,你可以使用之前讨论过的 AndFTP 应用从第二章树莓派服务器管理将手机上的视频上传到树莓派,然后使用媒体中心观看电影。你需要在树莓派上添加一个目录,将这些文件上传为 Kodi 中的媒体位置。转到视频 | 文件 | 文件,然后导航到添加视频...。在这里,你应该首先选择浏览,然后根文件系统。请注意,我们在第二章树莓派服务器管理中使用/home/pi作为上传目标。即使在这种情况下也应该可以工作。浏览到这个位置,然后在所有三个弹出窗口上点击确定。现在你应该在 Kodi 的视频列表中看到树莓派。你甚至可以将此文件夹添加到收藏夹以便轻松访问。打开 Kore 远程控制应用程序,并浏览到视频下的pi文件夹。当 Kodi 中的pi文件夹被突出显示时,在 Kore 远程控制应用程序中按下属性按钮。然后使用 Kore 上的箭头向下滚动选择添加到收藏夹

在 Android 设备上使用 Kodi 观看视频

Kore 中列出选择的按钮,即属性按钮

接下来,从第二章树莓派服务器管理中打开 AndFTP,并连接到树莓派,或者选择之前保存的连接。现在你应该看到/home/pi目录的内容,这是我们使用的用户pi的默认位置。这是目标位置。然后,在 AndFTP 的操作栏中选择手机图像,选择手机上的视频并将其上传到 Kodi。

在 Android 设备上使用 Kodi 观看视频

AndFTP 界面,从手机到树莓派选择上传位置

录制的视频通常位于DCIM/Camera下。选择你想要上传的视频。然后,在操作栏中点击上传图标:

在 Android 设备上使用 Kodi 观看视频

AndFTP 界面,从手机到树莓派开始上传

接下来,你可以在 Kodi 的视频部分中浏览到我们添加到pi目录的 Kodi,并查看你刚刚在媒体中心上传的视频。

将 Android 显示屏流式传输到 Kodi

另一件非常有趣的事情是,你可以流式传输你的 Android 屏幕,并让 Kodi 显示这个流。为此,我们首先需要从 Google Play 下载一个应用,该应用将流式传输 Android 显示屏,并在内部网络上使用 URL 发布它。我们将用于此目的的应用称为屏幕流式传输镜像,有免费和付费版本。对于这个项目来说,下载免费版本就足够了。启动应用后,你需要关闭一些广告,并在弹出窗口上按下立即开始按钮。

将 Android 显示屏流式传输到 Kodi

屏幕流式传输镜像

在这里,您将看到流媒体发布的地址。我们现在将在树莓派上的pi用户的home目录下保存这个rtsp://YOUR_ANDROID_IP_ADRESS:5000/screen链接,我们将把它保存在一个名为stream.strm的文件中。然后,在 Kodi 中浏览到pi目录,找到这个文件并打开它。请记住,我们已经将这个目录保存在 Kodi 的视频部分,并且也将其保存为收藏夹。现在,您应该能够看到您在 Android 设备屏幕上所做的任何事情,它连接到了 Kodi 使用的树莓派 HDMI 端口。另一个选项是通过这个通道显示 Android 相机捕获。我们使用的屏幕流镜像应用程序在 Android 通知区域有一个通知。如果您展开它,您将看到一个名为相机的选项。通过按下这个按钮,您将能够启动相机并查看相机捕获。

将 Android 显示流媒体到 Kodi

带有相机选项的屏幕流镜像通知

使用 NOOBS 安装媒体中心

在树莓派上安装媒体中心的另一个选项是使用 NOOBS。这样,用户可以非常容易地安装媒体中心,而不必担心与 Raspbian OS 相关的细节,就像我们在本章中所做的那样。我们已经在第一章中介绍了 NOOBS 的安装,从任何地方远程连接到您的 Pi。然而,在第一章中,从任何地方远程连接到您的 Pi,我们使用了离线安装选项。我们可以使用在线安装选项。您应该从downloads.raspberrypi.org/NOOBS_lite_latest下载在线 NOOBS 安装程序。这个 ZIP 文件要小得多,但在开始安装之前,您需要将树莓派连接到以太网网络。将文件的内容解压到 SD 卡上,并在插入这张 SD 卡后重新启动您的树莓派。现在,您将看到一个要安装的操作系统列表。列表中还包含两个媒体中心。这些是OpenELECOSMC。它们都基于 Kodi,我们在本章中已经介绍过。

摘要

这一章很短,但很有趣。我们已经学会在树莓派上安装并设置了一个最广泛使用的媒体中心,并可以远程从我们的 Android 设备控制它。

在下一章中,我们将动手开始一些 Python 和 Android 编程,并利用更多的连接可能性来连接树莓派和 Android。

第五章:使用树莓派的未接来电

在本章中,我们将实施一个更加面向编程的项目,并深入研究蓝牙智能或蓝牙低功耗(BLE)编程。我们将通过蓝牙使树莓派和 Android 手机进行通信,并使用这个通道控制树莓派。本章将涵盖以下主题:

  • 安装必要的组件

  • 向蓝牙低功耗添加传感器服务

  • 从 Android 应用程序连接

  • 从您的 Android 手机发送重启命令到树莓派

  • 从您的 Android 手机发送更多命令到树莓派

安装必要的组件

这个项目所需的硬件组件是一个支持 BLE 的蓝牙 USB 适配器。重要的是这个硬件支持 BLE,因为我们将专门利用蓝牙堆栈的这一部分。我们将使用由Plugable提供的一个,它在亚马逊上有售。

安装必要的组件

由 Plugable 提供的蓝牙适配器

我们已经下载的 Raspbian 发行版已经包含了对蓝牙的支持,但我们需要更新蓝牙软件包以获得更好的低功耗支持。您可以使用以下命令构建和安装更现代的蓝牙软件包版本:

sudo apt-get install libdbus-1-dev libdbus-glib-1-dev libglib2.0-dev libical-dev libreadline-dev libudev-dev libusb-dev make
mkdir -p work/bluepy
cd work/bluepy
wget https://www.kernel.org/pub/linux/bluetooth/bluez-5.33.tar.xz
tar xvf bluez-5.33.tar.xz
cd bluez-5.33
./configure --disable-systemd
make
sudo make install

make步骤将编译树莓派所需的必要软件包,并需要大约 15 分钟才能完成。但是,您需要耐心等待,因为最终会得到一些很酷和有用的东西。请注意,撰写本书时 BlueZ 的最新版本是 5.33,您可以通过检查www.kernel.org/pub/linux/bluetooth/上所有可用版本的列表来替换为最新版本。请注意,我们已使用--disable-systemd选项禁用了systemd支持,否则会导致构建错误。

前面的命令还安装了一些命令行工具,让我们能够配置和扫描蓝牙设备。以下命令列出了树莓派的 USB 端口上连接的所有组件:

lsusb

前面命令的输出如下:

Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 0a5c:21e8 Broadcom Corp.
Bus 001 Device 005: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter

在我的情况下,蓝牙适配器的名称是Broadcom。要获取有关特定设备的更多详细信息,请使用以下命令:

sudo lsusb -v -d 0a5c:

在这里,请注意0a5c是我正在重用的蓝牙适配器地址的第一部分,仅获取有关此设备的更多信息。

hciconfig工具将向您显示哪些设备支持蓝牙。这个命令在我的系统上输出了以下信息:

hci0:   Type: BR/EDR  Bus: USB
 BD Address: 5C:F3:70:68:BE:42  ACL MTU: 1021:8  SCO MTU: 64:1
 DOWN
 RX bytes:564 acl:0 sco:0 events:29 errors:0
 TX bytes:358 acl:0 sco:0 commands:29 errors:0

在这里看到,设备标记为DOWN。我们将保持这种状态,因为我们安装的下一个工具需要它最初处于关闭状态。

注意

有一些有用的蓝牙低功耗命令,您可以使用它们来检查其他 BLE 设备。我们暂时不会使用这些命令,但是熟悉它们并检查您的 BLE 设备是否工作或可访问是一个好习惯。

我们之前使用过的hciconfig工具可以帮助我们启动蓝牙设备。但是,如果您想继续本章的其余部分,不要这样做,因为下一个工具需要它处于关闭状态:

sudo hciconfig hci0 up

将此命令放入 crontab 中是个好主意,如前所述,使用 crontab 和“-e”选项,以便您可以使用 nano 作为编辑器,并自动安装新的 crontab。在文件末尾添加@reboot sudo hciconfig hci0 up,然后保存并退出。

还有两个其他命令可以使用:

sudo hcitool lescan

这个命令列出了 BLE 设备。现在让我们看看以下命令:

sudo hcitool lecc 68:64:4B:0B:24:A7

这个命令测试与设备的蓝牙连接。请注意,后一个命令提供的地址是由前一个命令返回的。

我们甚至需要一个蓝牙的编程支持。我们将使用Go作为语言,Gatt包用于 Go 语言,为 Go 语言提供了对蓝牙低功耗的支持。通用属性配置文件Gatt)是一个通用规范,用于在 BLE 链路上发送和接收小量数据,称为属性。让我们运行以下命令来安装go语言:

cd
git clone https://go.googlesource.com/go
cd go
git checkout go1.4.1
cd src
./all.bash

在这里你可能想去拿杯咖啡,因为最后一个命令将花费大约 40 分钟的时间。在输出的末尾,你将看到go安装程序要求你将一个二进制目录添加到你的路径中,以便轻松访问。以下命令可以实现这一点:

PATH=$PATH:/home/pi/go/bin
export PATH
export GOROOT=/home/pi/go
export GOPATH=/home/pi/gopath

提示

将这些命令放在/etc/profile文件中是个好主意,这样你就可以在将来每次启动会话时执行它们。但是一定要将它们添加到文件的末尾。此外,即使你已经将它们放在profile文件中,也不要忘记实际执行它们,如果你想在不重新启动的情况下继续。

然后,使用以下命令下载 Gatt 包源文件:

go get github.com/paypal/gatt

现在我们将使用以下命令启动一个简单的 BLE 服务器:

cd /home/pi/gopath/src/github.com/paypal/gatt
go build examples/server.go
sudo ./server

提示

完成本章后,你可能想使用以下命令将服务器启动命令放在crontab中:

crontab -e

这样,每次重新启动 Pi 时,BLE 服务器都会启动。在末尾添加以下行:

@reboot sudo /home/pi/gopath/src/github.com/paypal/gatt/server

现在是时候找到我们的树莓派了,它在安卓上表现得像一个 BLE 设备。我们将使用BluePixel TechnologiesBLE Scanner应用程序,它可以在 Play 商店上找到。当你启动它时,你将看到周围可用的 BLE 设备列表以及它们的地址。可以使用hciconfig命令查看 Pi 上蓝牙适配器的地址。Gatt 服务器的默认实现将设备命名为Gopher。以下截图说明了 BLE Scanner 应用程序,显示 Pi 作为 BLE 设备:

安装必要的组件

BLE Scanner 应用程序显示 Pi 作为 BLE 设备

BLE 堆栈设计成设备支持一些用户可以连接的服务,并且每个服务可以提供读/写或通知特性,这主要是你可以写入、读取或从中获取通知的数据。在应用程序中点击设备,你将连接到 Pi 新启动的 BLE 服务器。你将看到四个服务。我们感兴趣的是称为UNKNOWN SERVICE的服务,它没有名称,因为它不是标准服务,它只是用来演示 Gatt 示例服务器。点击这个服务,你将看到这个服务提供的三个特性:READWRITENotification。你可以通过查看 BLE Scanner 应用程序上哪个按钮被启用来识别特性的类型。以下截图说明了 READ 特性:

Installing the necessary components

READ 特性

向蓝牙低功耗添加传感器服务

我们将向 Gatt 的已有示例添加一个新服务。这个新服务将首先发布两个新特性:一个用于湿度,另一个用于温度测量。我们将使用我们在第二章中讨论过的技术以相同的方式读取测量值,使用 Pi 进行服务器管理。要读取这些测量值,我们将创建两个内容类似于我们在第二章中讨论的sense.py文件的新文件。让我们在home目录下创建两个文件,分别命名为humidity.pytemperature.pytemperature.py文件的内容如下:

#!/usr/bin/python

import sys
import Adafruit_DHT

humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, 4)
print str(temperature)

humidity.py文件的内容类似。唯一的区别是它打印出测量的湿度部分而不是温度:

#!/usr/bin/python

import sys
import Adafruit_DHT

humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, 4)
print str(humidity)

我们还需要使用以下命令将文件访问模式更改为可执行:

chmod +x temperature.py humidity.py

现在,您可以使用以下命令测试传感器测量:

sudo ./temperature.py
sudo ./humidity.py

下一步是通过蓝牙通道发布这些读数。我们将在现有的 Gatt 服务器示例中创建一个新服务。为此,您可以开始编辑/home/pi/gopath/src/github.com/paypal/gatt/examples路径中服务器示例的server.go源文件。您只需要在onStateChanged函数定义中添加三行代码,放在其他服务定义之间。在以下内容中,请注意计数服务和电池服务已经存在。我们只需要添加传感器服务:

// A simple count service for demo.
s1 := service.NewCountService()
d.AddService(s1)

// A sensor service for demo.
sSensor := service.NewSensorService()
d.AddService(sSensor)

// A fake battery service for demo.
s2 := service.NewBatteryService()
d.AddService(s2)

此外,在同一文件中,更改广告新服务的行为以下代码,以便也广告新服务:

// Advertise device name and service's UUIDs.
d.AdvertiseNameAndServices("Gopher", []gatt.UUID{s1.UUID(), sSensor.UUID(), s2.UUID()})

我们还需要添加新服务的定义。以下代码应放在名为sensor.go的文件中,放在 Gatt 示例的service目录下,与其他服务定义文件(如count.gobattery.go)处于同一级别:

package service

import (
 "fmt"
 "log"
 "os/exec"
 "strings"

 "github.com/paypal/gatt"
)

func NewSensorService() *gatt.Service {
 s := gatt.NewService(gatt.MustParseUUID("19fc95c0-c111-11e3-9904- 0002a5d5c51b"))
 s.AddCharacteristic(gatt.MustParseUUID("21fac9e0-c111-11e3-9246- 0002a5d5c51b")).HandleReadFunc(
  func(rsp gatt.ResponseWriter, req *gatt.ReadRequest) {
   out, err := exec.Command("sh", "-c", "sudo /home/pi/temperature.py").Output()
    if err != nil {
     fmt.Fprintf(rsp, "error occured %s", err)
     log.Println("Wrote: error %s", err)
    } else {
     stringout := string(out)
     stringout = strings.TrimSpace(stringout)
     fmt.Fprintf(rsp, stringout)
     log.Println("Wrote:", stringout)
    }
 })

 s.AddCharacteristic(gatt.MustParseUUID("31fac9e0-c111-11e3-9246- 0002a5d5c51b")).HandleReadFunc(
  func(rsp gatt.ResponseWriter, req *gatt.ReadRequest) {
   out, err := exec.Command("sh", "-c", "sudo /home/pi/humidity.py").Output()
    if err != nil {
     fmt.Fprintf(rsp, "error occured %s", err)
     log.Println("Wrote: error %s", err)
    } else {
     stringout := string(out)
     stringout = strings.TrimSpace(stringout)
     fmt.Fprintf(rsp, stringout)
     log.Println("Wrote:", stringout)
   }
 })

 return s
}

我们需要使用go构建和重新运行我们的服务器代码。我们之前使用的以下命令将帮助我们做到这一点。请注意,您应该在/home/pi/gopath/src/github.com/paypal/gatt目录中:

go build examples/server.go
sudo ./server

我们可以再次在 Android 上使用 BLE Scanner 应用程序连接到这项新服务并读取温度和湿度传感器数值。以下截图说明了 Gopher 服务:

向蓝牙低功耗添加传感器服务

连接到 Gopher 设备后,您应该看到具有19fc95c0-c111-11e3-9904-0002a5d5c51b ID 的新添加服务,以及该服务的新特征,如下截图所示:

向蓝牙低功耗添加传感器服务

新增特征:一个用于温度,另一个用于湿度测量

按下读取按钮后,以下截图说明了温度测量的特征细节:

向蓝牙低功耗添加传感器服务

温度测量特征显示当前值为 27 度

从 Android 应用程序连接

我们已经使用现有的应用程序连接到了我们在树莓派上实现的 BLE 服务。这个名为 BLE Scanner 的应用程序非常通用,可以用于任何类型的 BLE 设备。但是,我们需要一个更专门的应用程序,它只读取测量值并抽象出 BLE 协议的细节,如设备扫描、服务和服务特征。在本节中,我们将实现一个 Android 应用程序来连接到树莓派的 BLE。为此,我们需要安装 Android Studio。Android Studio 是由 Google 专门为 Android 应用程序开发设计的。您可以通过访问developer.android.com/tools/studio/了解更多信息。您可以在developer.android.com/sdk/找到安装说明。我们将使用真实设备来测试我们的应用程序,而不是内置的模拟器。为此,您可能需要安装特定于您的 Android 手机的设备驱动程序,并对 Android Studio 安装进行配置更改。developer.android.com/tools/device.html链接将帮助您执行这些操作。

现在,启动 Android Studio 并选择创建一个新项目。我将应用程序命名为BLEPi,域名为example.com。您应该选择手机和平板电脑作为表单因素,至少Android 5.0作为最低 SDK,因为该 SDK 引入了更好的 BLE 支持到 Android 系统。核心 BLE 支持实际上是在 Android 4.3 中添加的,本书网站上分发的代码文件以及本书的 GitHub 存储库也适用于 Android 4.3 和 Android 5.0。然而,为了简单和方便起见,即将介绍的代码仅适用于 Android 5.0。请注意,在安装 Android Studio 时,您应该已经下载了 Android 5.0 SDK,以便能够在创建项目向导中选择它。请查看本节中刚提到的链接,以获取有关此问题的更多详细信息。然后,选择向应用程序添加一个空白活动,并在下一步中不更改活动的名称;我们将保持其为MainActivity

我们将通过向AndroidManifest.xml文件中的manifestapplication标签之间添加蓝牙权限来开始我们的实现:

<uses-permission 
android:name="android.permission.BLUETOOTH"/>
<uses-permission 
android:name="android.permission.BLUETOOTH_ADMIN"/>

然后,我们将开始对MainActivity.java文件进行更改。首先进行以下类变量定义:

private BluetoothAdapter bluetoothAdapter;
private BluetoothLeScanner bleScanner;
private BluetoothGatt bleGatt;
private static final int REQUEST_ENABLE_BT = 1;
private static final UUID UUID_Service = 
UUID.fromString("19fc95c0-c111-11e3-9904-0002a5d5c51b");
private static final UUID UUID_TEMPERATURE = 
UUID.fromString("21fac9e0-c111-11e3-9246-0002a5d5c51b");
private static final UUID UUID_HUMIDITY = 
UUID.fromString("31fac9e0-c111-11e3-9246-0002a5d5c51b");

bluetoothAdapter定义表示本地设备的蓝牙适配器,并允许您执行基本的蓝牙任务,比如发现其他设备和获取已发现设备的属性。bleScanner提供了执行与蓝牙 LE 设备特定的扫描相关操作的方法,bleGatt提供了蓝牙 GATT 功能,以实现与蓝牙智能设备的通信。我们在这里定义的 UUID 与我们之前在 Pi 上保存的sensor.go文件中使用的 UUID 相同,用于识别新服务及其两个新特征。

提示

在 Android Studio 中,您可以使用Alt+Enter快捷键自动导入丢失的包。光标应该位于 java 文件中缺少导入的类上。或者,将光标放在类上,将鼠标指针放在上面,您将看到一个灯泡菜单。在这个菜单中,您可以选择导入类选项。

onCreate方法中,当应用程序第一次启动时,Android 系统会调用该方法,我们可以初始化bluetoothAdapter

BluetoothManager bluetoothManager = 
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();

我们需要定义startScan方法,每当我们想要启动 BLE 设备扫描时就会调用该方法。

private void startScan() {
   if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled())
   {
   Intent enableBtIntent = 
      new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
   startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
   }  else {
   bleScanner = bluetoothAdapter.getBluetoothLeScanner();
      if (bleScanner != null) {
          final ScanFilter scanFilter = 
             new ScanFilter.Builder().build();
         ScanSettings settings = 
             new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();
         bleScanner.startScan(
             Arrays.asList(scanFilter), settings, scanCallback);
      }
   }
}

在这里,我们首先检查设备上是否启用了蓝牙。如果没有,我们将显示一个消息框,让用户启用蓝牙。如果启用了,我们将获取bleScanner的一个实例,该实例用于使用startScan方法开始扫描。我们可以给一个回调实现名称,比如scanCallback,每当扫描返回一些结果时就会调用该方法。现在,我们需要定义这个回调变量,如下面的代码所示:

private ScanCallback scanCallback = new ScanCallback() {
   @Override
   public void onScanResult(int callbackType, ScanResult result) {
      if("Gopher".equals(result.getDevice().getName())) {
          Toast.makeText(MainActivity.this, "Gopher found", 
             Toast.LENGTH_SHORT).show();
          if(bleScanner != null) {
             bleScanner.stopScan(scanCallback);
          }
         bleGatt = 
            result.getDevice().connectGatt(
                getApplicationContext(), false, bleGattCallback);
       }
       super.onScanResult(callbackType, result);
    }
};

ScanCallback实现覆盖了一个重要的方法onScanResult,每当有新设备报告时就会调用该方法。然后我们检查设备名称是否与在 Pi 上的server.go文件中定义的名称相同。如果是,我们可以将设备属性和连接信息保存到bleGatt变量中。我们甚至可以使用connectGatt方法连接到设备,并提供另一个回调实现bleGattCallback,每当 Android 系统与设备建立连接时就会调用该方法。如果找到了我们要找的设备,我们就停止扫描。这是这个回调的定义:

private BluetoothGattCallback bleGattCallback = new BluetoothGattCallback() {
   @Override
   public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
      gatt.discoverServices();
      super.onConnectionStateChange(gatt, status, newState);
   }

   @Override
   public void onServicesDiscovered(BluetoothGatt gatt, int status) {
      BluetoothGattService service = 
         gatt.getService(UUID_Service);
      BluetoothGattCharacteristic temperatureCharacteristic = 
         service.getCharacteristic(UUID_TEMPERATURE);
      gatt.readCharacteristic(temperatureCharacteristic);
      super.onServicesDiscovered(gatt, status);
   }

   @Override
   public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) {
      final String value = characteristic.getStringValue(0);
      runOnUiThread(new Runnable() {
         @Override
         public void run() {
            TextView tv;
            if(UUID_HUMIDITY.equals(characteristic.getUuid())) {
                tv = (TextView) MainActivity.this.findViewById(
                   R.id.humidity_textview);
            } else {
                tv = (TextView) MainActivity.this.findViewById(
                   R.id.temperature_textview);
              }
             tv.setText(value);
        }
      });

      BluetoothGattService service = 
         gatt.getService(UUID_Service);
      readNextCharacteristic(gatt, characteristic);
      super.onCharacteristicRead(gatt, characteristic, status);
   }
};

在这个回调实现中,我们重写了三个在不同时间从 Android 系统调用的重要方法。每当通过蓝牙与远程设备建立连接时,将调用onConnectionStateChange方法。在这种情况下,我们可以使用discoverServices方法启动设备的服务发现。然后,当设备上发现服务时,将调用onServicesDiscovered方法。在这种情况下,我们将首先读取我们在树莓派上定义的传感器服务的温度特征,使用readCharacteristic方法。每当特征读取操作的值成功时,将调用第三个重写的方法onCharacteristicRead,在其中我们读取下一个特征,即湿度,然后在同一方法中等待此操作成功。然后,我们轮流使用readNextCharacteristic方法读取湿度和温度值,我们将在相同的回调实现中定义该方法。这是因为 BLE 协议不允许我们同时读取两个特征。让我们看一下以下代码:

private void readNextCharacteristic(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
   BluetoothGattService service = gatt.getService(UUID_Service);
   if (UUID_HUMIDITY.equals(characteristic.getUuid())) {
       BluetoothGattCharacteristic temperatureCharacteristic = 
          service.getCharacteristic(UUID_TEMPERATURE);
       gatt.readCharacteristic(temperatureCharacteristic);
   } else {
      BluetoothGattCharacteristic humidityCharacteristic = 
         service.getCharacteristic(UUID_HUMIDITY);
      gatt.readCharacteristic(humidityCharacteristic);
     }
}

每当相应的读操作成功时,我们使用返回的characteristic对象的getStringValue方法获取测量值,然后在我们将在activity_main.xml文件中定义的 UI 元素中显示它,如下所示:

<TextView
        android:id="@+id/temperature_textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true" />

    <TextView
        android:id="@+id/humidity_textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

为了使代码完整,我们还需要在MainActivity.java文件中定义以下方法:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if(requestCode == REQUEST_ENABLE_BT) {
      startScan();
   }
   super.onActivityResult(requestCode, resultCode, data);
}

@Override
protected void onResume() {
   startScan();
   super.onResume();
}

@Override
protected void onPause() {
   if(bleScanner != null) {
      bleScanner.stopScan(scanCallback);
      }

   if (bleGatt != null) {
       bleGatt.close();
       bleGatt.disconnect();
       bleGatt = null;
   }
   super.onPause();
}

每当用户启用蓝牙时,将调用onActivityResult方法,我们需要在这种情况下开始扫描,以及每当用户启动调用onResume的应用程序时。如果用户关闭应用程序,可以通过onPause方法停止蓝牙连接。

这是一个很好的机会,测试我们迄今为止实施的应用的第一个版本,并验证它是否有效。在 Android Studio 的运行菜单中选择运行应用程序,然后您将有选择安装应用程序的位置的选项。然后,您将在列表中看到连接到计算机的 Android 设备。

从您的 Android 手机向树莓派发送重启命令

到目前为止,我们一直通过 BLE 从树莓派接收数据。现在,我们将使用相同的通道向其发送命令。我们将在与我们的温度和湿度读特征相同的服务中实现一个新的写特征,这些特征是在树莓派上定义的。使用这些新特征,我们将向树莓派发送重启命令。让我们从再次编辑sensor.go文件开始,并在其末尾放入以下代码:

s.AddCharacteristic(gatt.MustParseUUID("41fac9e0-c111-11e3-9246- 0002a5d5c51b")).HandleWriteFunc(
  func(r gatt.Request, data []byte) (status byte) {
   log.Println("Command received")
   exec.Command("sh", "-c", "sudo reboot").Output()
   return gatt.StatusSuccess
 })

使用以下命令构建和重新启动 BLE 服务器:

cd /home/pi/gopath/src/github.com/paypal/gatt
go build examples/server.go
sudo ./server

现在,使用 BLE Scanner 应用程序测试先前提到的特征。每当您向这些特征写入内容时,树莓派将重新启动。

下一步是在我们一直在构建的 Android 应用程序中实现这个新的重启功能。

首先,添加我们刚刚定义的新写特征的 UUID 以及控制操作顺序的变量,如下所示:

private static final UUID UUID_REBOOT = 
   UUID.fromString("41fac9e0-c111-11e3-9246-0002a5d5c51b");
private volatile boolean isSendReboot = false;

布尔变量isSendReboot将用于启动写特征操作并与先前定义的读操作一起进行编排。BLE 堆栈无法处理彼此太接近的读/写操作,我们希望在上一个操作完成之前避免执行下一个操作。然后,在bleGattCallbackonCharacteristicRead函数中,将我们调用readNextCharacteristic的行更改为以下代码:

if(isSendReboot) {
   BluetoothGattCharacteristic rebootCharacteristic = 
      service.getCharacteristic(UUID_REBOOT);
   rebootCharacteristic.setValue("reboot");
   gatt.writeCharacteristic(rebootCharacteristic);
} else {
   readNextCharacteristic(gatt, characteristic);
}

在这里,如果设置了控制变量,我们将向重启特征写入值reboot,通过点击我们即将实现的按钮。我们可以重写bleGattCallback中的另一个方法:

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
   isSendReboot = false;
   readNextCharacteristic(gatt, characteristic);
   super.onCharacteristicWrite(gatt, characteristic, status);
}

当我们重置控制变量并继续读取操作时,将调用此方法以确保写入特征操作成功。细心的人可能会发现这段代码存在一个小问题,即我们正在向 Pi 发送重新启动命令,但与此同时,我们还试图从位于同一设备上的蓝牙设备读取特征。当 Pi 重新启动时,这些读取将无法工作,如果我们在重新启动成功完成后不关闭并重新打开应用程序,我们的应用程序将无法重新连接。解决此问题将留给您作为练习。

实现的最后一部分是向我们的用户界面添加一个命令按钮,并将此按钮连接到MainAcitivity.java文件中的一个方法,每当按钮被按下时都会执行。首先在activity_main.xml文件的RelativeLayout标签内添加以下行:

<Button
        android:id="@+id/reboot_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/humidity_textview"
        android:text="Reboot"
        android:onClick="sendRebootCommand"
        android:enabled="false"/>

MainActivity.java文件中定义sendRebootCommand方法:

public void sendRebootCommand(View v) throws InterruptedException
{
isSendReboot = true;
}

当单击重新启动按钮时,此函数唯一要做的事情就是设置我们之前定义的控制变量。

您还可以在ScanCallback类实例的onScanResult方法中添加以下代码,以在通过蓝牙连接到树莓派时启用按钮:

if(bleGatt != null) {
   MainActivity.this.findViewById(R.id.reboot_button).setEnabled(true);
}

这是一个再次测试应用程序的好地方,看看您是否可以通过 Android 设备成功重新启动 Pi。

从您的 Android 手机发送更多命令到 Pi

在上一节中,我们已经从 Android 发送了重新启动命令到 Pi。在本节中,我们将发送两个新命令。一个是点亮我们将连接到 Pi 的 LED,另一个是在 Pi 上播放声音。这些命令将在接下来的章节中被重复使用。

点亮 LED 灯

我们将首先将 LED 灯连接到 Pi 的 GPIO 端口。LED 通常带有短腿和长腿。将电阻连接到 LED 的短腿,然后将一个母头/母头跳线连接到电阻的另一端。然后将这个跳线连接到 Pi 的一个地针上。查看第二章中的模式,使用 Pi 进行服务器管理,以识别针脚。请注意,当我们将温湿度传感器连接到 Pi 时,我们已经使用了一个地针。但是,有很多地针可用。将 LED 的长腿连接到 GPIO 针脚之一。我们将选择编号为17的针脚。您可以查看第二章中的 GPIO 端口映射图,以识别端口17

提示

最好选择一个电阻在 270Ω到 470Ω之间。这个电阻可以保护 LED 灯免受意外电压变化的影响。如果您选择电阻值较低的电阻,LED 将会更亮。

我们将使用一个名为wiringPi的软件实用程序来访问 GPIO 和 LED 灯。我们可以使用以下命令下载和安装它:

cd
git clone git://git.drogon.net/wiringPi
cd wiringPi
./build

这些命令已经帮助我们安装了一个名为gpio的命令行工具,您现在可以使用它来点亮 LED 灯:

gpio -g mode 17 out
gpio -g write 17 1

您可以使用以下命令关闭它:

gpio -g write 17 0

我们需要向 BLE 服务器实现添加两个新特征:一个用于打开灯,另一个用于关闭灯。在sensor.go文件的末尾添加以下行,并注意我们为每个新创建的特征都有新的 UUID:

s.AddCharacteristic(gatt.MustParseUUID("51fac9e0-c111-11e3-9246-0002a5d5c51b")).HandleWriteFunc(
  func(r gatt.Request, data []byte) (status byte) {
   log.Println("Command received to turn on")
   exec.Command("sh", "-c", "gpio -g mode 17 out").Output()
   exec.Command("sh", "-c", "gpio -g write 17 1").Output()
   return gatt.StatusSuccess
 })

 s.AddCharacteristic(gatt.MustParseUUID("61fac9e0-c111-11e3-9246-0002a5d5c51b")).HandleWriteFunc(
  func(r gatt.Request, data []byte) (status byte) {
   log.Println("Command received to turn off")
   exec.Command("sh", "-c", "gpio -g mode 17 out").Output()
   exec.Command("sh", "-c", "gpio -g write 17 0").Output()
   return gatt.StatusSuccess
 })

现在,再次构建和重启 BLE 服务器。如果你已经将 BLE 服务器命令添加到 crontab 中,你可能需要重新启动树莓派。接下来,再次使用 BLE Scanner 应用连接到树莓派,并在应用程序中的特性部分使用Write按钮向这些特性写入值。你需要提供一些文本来写入,否则 BLE Scanner 应用将不会发送命令。一旦你这样做了,你就可以打开和关闭 LED 灯。

提示

在尝试使用我们正在构建的应用程序访问之前,最好在 BLE Scanner 应用中检查你已经在树莓派上添加的新特性。这样,我们就可以确保我们已经正确地在树莓派端添加了特性。

下一步是在我们的应用程序中实现这个新功能。我们可以从activity_main.xml文件中引入两个新按钮开始:

<Button
        android:id="@+id/turnon_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/reboot_button"
        android:text="Turn on"
        android:onClick="sendTurnOnCommand"
        android:enabled="false"/>

    <Button
        android:id="@+id/turnoff_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/turnon_button"
        android:text="Turn off"
        android:onClick="sendTurnOffCommand"
        android:enabled="false"/>

MainActivity.java中,为新特性定义新的 UUID 和控制变量:

private static final UUID UUID_TURNON = 
   UUID.fromString("51fac9e0-c111-11e3-9246-0002a5d5c51b");
private static final UUID UUID_TURNOFF = 
   UUID.fromString("61fac9e0-c111-11e3-9246-0002a5d5c51b");
private volatile boolean isSendTurnOn = false;
private volatile boolean isSendTurnOff = false;

scanCallbackonScanResult方法中,在启用重启按钮后,添加以下代码以启用这两个按钮:

MainActivity.this.findViewById(R.id.turnon_button).setEnabled(true);

MainActivity.this.findViewById(R.id.turnoff_button).setEnabled(true);

bleGattCallbackonCharacteristicRead方法中,为isSendReboot的控制变量的现有检查添加新的 else-if 语句。新代码将类似于以下内容:

if(isSendReboot) {
   BluetoothGattCharacteristic rebootCharacteristic = 
      service.getCharacteristic(UUID_REBOOT);
   rebootCharacteristic.setValue("reboot");
   gatt.writeCharacteristic(rebootCharacteristic);
} else if(isSendTurnOn) {
   BluetoothGattCharacteristic turnOnCharacteristic = 
      service.getCharacteristic(UUID_TURNON);
   turnOnCharacteristic.setValue("turnon");
   gatt.writeCharacteristic(turnOnCharacteristic);
} else if(isSendTurnOff) {
   BluetoothGattCharacteristic turnOffCharacteristic = 
      service.getCharacteristic(UUID_TURNOFF);
   turnOffCharacteristic.setValue("turnoff");
   gatt.writeCharacteristic(turnOffCharacteristic);
} else {
   readNextCharacteristic(gatt, characteristic);
}

onCharacteristicWrite方法中,添加以下代码片段以重置控制变量:

isSendTurnOn = false;
isSendTurnOff = false;

最后,在新按钮的点击事件上添加可以调用的新函数:

public void sendTurnOnCommand(View v) throws InterruptedException
{
   isSendTurnOn = true;
}

public void sendTurnOffCommand(View v) throws InterruptedException
{
   isSendTurnOff = true;
}

你的应用将类似于以下截图:

点亮 LED 灯

应用的最终版本

点击按钮后,请耐心等待新按钮的效果,因为消息需要几秒钟才能到达树莓派,并且 LED 灯需要点亮。

在树莓派上播放声音

为了能够在树莓派上播放声音,声音模块应该在重启时加载。为了做到这一点,我们需要将声音模块的规格添加到/etc/modules文件中。如果文件中不存在snd-bcm2835,则需要在文件中添加这一规格。

提示

你可以使用lsmod命令行工具查看当前加载的模块:

sudo modprobe snd_bcm2835

这个命令在不重启的情况下加载声音模块,以使/etc/modules文件的内容生效。

我们甚至需要找到一个可以播放的音频文件,可以使用以下命令进行下载:

cd
wget http://www.freespecialeffects.co.uk/soundfx/sirens/whistle_blow_01.wav

现在你可以使用以下命令播放这个声音:

aplay whistle_blow_01.wav

提示

请注意,由于 HDMI 输出,音频通道可能会默认,你可能无法在 3.5mm 插孔上听到任何声音。在这种情况下,你可以运行以下命令将默认音频播放器设置为 3.5mm 插孔:

amixer cset numid=3 1

下一步是将新的写特性添加到sensor.go文件中,如下所示:

s.AddCharacteristic(gatt.MustParseUUID("71fac9e0-c111-11e3-9246-0002a5d5c51b")).HandleWriteFunc(
  func(r gatt.Request, data []byte) (status byte) {
   log.Println("Command received to whistle ")
   exec.Command("sh", "-c", "aplay /home/pi/whistle_blow_01.wav").Output()
   return gatt.StatusSuccess
 })

不要忘记使用go build examples/server.go命令构建和重启树莓派。接下来,在activity_main.xml文件中定义一个新按钮:

<Button
        android:id="@+id/whistle_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/turnoff_button"
        android:text="Whistle"
        android:onClick="sendWhistleCommand"
        android:enabled="false"/>

MainActivity.java文件中为onClick事件定义一个新的事件处理程序:

public void sendWhistleCommand(View v) throws InterruptedException
{
    isSendWhistle = true;
}

接下来,将新的 UUID 和控制变量添加到同一个文件中:

private static final UUID UUID_WHISTLE = 
   UUID.fromString("71fac9e0-c111-11e3-9246-0002a5d5c51b");
private volatile boolean isWhistle = false;

scanCallback实例变量的onScanResult方法中,在bleGatt的 null 检查的 if 语句中启用新按钮:

MainActivity.this.findViewById(R.id.whistle_button).setEnabled(true);

bleGattCallback变量的onCharacteristicRead处理程序中的新 else-if 语句中添加以下代码:

else if(isSendWhistle) {
   BluetoothGattCharacteristic whistleCharacteristic = 
      service.getCharacteristic(UUID_WHISTLE);
   whistleCharacteristic.setValue("whistle");
   gatt.writeCharacteristic(whistleCharacteristic);
}

onCharacteristicWrite方法中添加一个新的语句以重置控制变量:

isSendWhistle = false;

现在哨声命令已经准备好从我们的应用程序中进行测试。

结合命令并在来电时获得通知

在最后一节中,我们将结合哨声和 LED 点亮命令,并在手机响铃时启动这个新命令。到目前为止,我们已经习惯了创建新特性。这里是要添加到sensor.go文件中的新特性:

s.AddCharacteristic(gatt.MustParseUUID("81fac9e0-c111-11e3-9246-0002a5d5c51b")).HandleWriteFunc(
  func(r gatt.Request, data []byte) (status byte) {
   log.Println("Command received to turn on and whistle")
   exec.Command("sh", "-c", "aplay /home/pi/whistle_blow_01.wav").Output()
   exec.Command("sh", "-c", "gpio -g mode 17 out").Output()
   exec.Command("sh", "-c", "gpio -g write 17 1").Output()
   return gatt.StatusSuccess
 })

我们可以将这两个命令组合起来,以免自己从发送两个单独命令的开发细节中解脱出来。我们需要在AndroidManifest.xml文件中获取来自 Android 系统的来电状态的新权限:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

我们还需要在MainActivity.java中添加新的实例变量:

private static final UUID UUID_WHISTLE_AND_TURNON = 
   UUID.fromString("81fac9e0-c111-11e3-9246-0002a5d5c51b");
private volatile boolean isSendWhistleAndTurnOn = false;

然后,我们需要获取系统电话服务的实例,并将我们自己的监听器附加到它上面。在onCreate方法中添加这两行代码:

TelephonyManager TelephonyMgr = (TelephonyManager) 
   getSystemService(Context.TELEPHONY_SERVICE);
TelephonyMgr.listen(new PhoneListener(), 
PhoneStateListener.LISTEN_CALL_STATE);

接下来,定义一个本地的PhoneListener类:

class PhoneListener extends PhoneStateListener {
   public void onCallStateChanged(int state, String incomingNumber) {
      super.onCallStateChanged(state, incomingNumber);
      switch (state) {
         case TelephonyManager.CALL_STATE_RINGING:
         Toast.makeText(getApplicationContext(), incomingNumber, Toast.LENGTH_LONG).show();
         Toast.makeText(getApplicationContext(), "CALL_STATE_RINGING", Toast.LENGTH_LONG).show();
         isSendWhistleAndTurnOn = true;
         break;
      default:
         break;
      }
   }
}

在这里,每当我们在手机上得到一个状态变化时,我们会检查这是否是CALL_STATE_RINGING状态。如果是,我们可以像按钮点击事件处理程序为先前定义的命令一样设置新创建命令的控制变量。然后,我们也可以在onCharacteristic读取方法中添加这个额外的 else-if 语句:

else if(isSendWhistleAndTurnOn) {
   BluetoothGattCharacteristic whistleAndTurnOnCharacteristic = 
      service.getCharacteristic(UUID_WHISTLE_AND_TURNON);
   whistleAndTurnOnCharacteristic.setValue("whistleturnon");
   gatt.writeCharacteristic(whistleAndTurnOnCharacteristic);
}

接下来,我们将在onCharacteristicWrite方法中重置控制变量如下:

isSendWhistleAndTurnOn = false;

现在,当您的手机响铃时,您将能够看到 LED 灯亮起并在树莓派上听到哨声。请注意,我们的应用程序需要启动并可见才能正常工作。这是由我们代码中的两个主要问题之一引起的。所有通过 BLE 与树莓派的通信实际上应该在 Android 服务中进行,电话事件需要在BroadcastReceiver中处理,而不是在activity中。这两个实现,即树莓派通信和电话状态拦截,实际上应该与activity分开。活动实际上应该是一个 UI 组件,仅此而已。然而,我们在这里的意图是只向您展示有趣的部分,并快速粗糙地完成。这些对 Android 代码的进一步改进将留作您的练习。

总结

在本章中,我们涵盖了很多内容,从树莓派上的 BLE 实现到 Android BLE 代码的细节。我们在树莓派上玩得很开心,并提出了一个可以进一步开发的有用项目。

在下一章中,我们将学习更多的方法来利用树莓派上的 BLE 设备,并且不仅将我们的手机作为 Android 设备,还将其作为树莓派的访问点。

第六章:车载树莓派

在本章中,我们将继续在树莓派上使用蓝牙来跟踪我们汽车的位置和数据。本章将涵盖以下部分:

  • 查找汽车位置

  • 使用您的 Android 设备作为访问点

  • 收集汽车数据

  • 将数据发送到云端

  • 把所有东西放在一起

查找汽车位置

在本章中,我们将从我们的汽车收集发动机数据,但如果我们也能收集一些位置数据,事情将变得更加有趣。为此,我们将连接一个 USB GPS 接收器到树莓派,并通过这个设备接收我们的位置。我们将使用市场上最便宜的接收器之一,如下图所示:

查找汽车位置

Globalsat BU-353 GPS 接收器

将 GPS 连接到树莓派后,您可以发出lsusb命令以查看它是否已正确注册。我的系统上此命令的输出如下,这里Prolific是 GPS 适配器:

Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
Bus 001 Device 005: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
Bus 001 Device 006: ID 0a5c:21e8 Broadcom Corp.

我们需要安装的下一件事是一个 GPS 守护程序,它从适配器接收位置信息:

sudo apt-get install gpsd gpsd-clients python-gps

您可能需要重新启动以启动守护程序。否则,您可以发出以下命令立即使其工作:

sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock

安装脚本甚至为我们提供了一个工具,通过基于文本的窗口查看当前 GPS 位置和范围内的卫星:

cgps –s

提示

GPS 接收器在室外或靠近窗户的地方可以最好地工作。

我系统上cgps命令的输出以及我的位置在以下截图中显示:

查找汽车位置

从 cgps –s 命令的输出

在这里,您可以看到我特别在我的视野中拥有的 GPS 卫星,以及我的纬度和经度以及其他通过 GPS 系统可用的有用信息。

提示

如果您从cgps命令中收到超时错误,您需要使用以下命令重新启动 GPS 守护程序:

sudo killall gpsd
sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock

即使您已经重新启动了树莓派,如果您仍然收到此超时错误,则可以将以下命令放入crontab中,但是,还有一个更好的地方可以放置这些命令,稍后将进行描述:

@reboot sudo killall gpsd
@reboot sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock

还可以从 Python 程序中以编程方式获取位置信息。我们稍后将利用这种可能性。但是现在,以下 Python 代码在名为getgps.py的文件中测试 Python gps库:

#! /usr/bin/python

from gps import *
import math

gpsd = gps(mode=WATCH_ENABLE) #starting the stream of info

count = 0
while count < 10:  # wait max 50 seconds
    gpsd.next()
    if gpsd.fix.latitude != 0 and not math.isnan(gpsd.fix.latitude) :
        print gpsd.fix.latitude,gpsd.fix.longitude
        break
    count = count + 1
    time.sleep(5)

这个小程序唯一的作用是在有报告时输出 GPS 位置。我们可以使用python getgps.py命令来调用它。

收集汽车数据

为了收集汽车数据,我们将使用大多数汽车上都有的标准车载 诊断OBD)接口,欧洲称为 OBD-II 或 EOBD。这些是用于连接到汽车 OBD 端口的等效标准;您还可以从该端口读取有关汽车的诊断数据和故障代码。

注意

1996 年,OBD-II 规范对所有在美国制造和销售的汽车都是强制性的。欧盟在 2001 年跟随步伐,要求所有在欧盟销售的汽油车辆都必须使用 EOBD,随后在 2003 年要求所有柴油车辆也要使用 EOBD。2010 年,HDOBD(重型)规范对在美国销售的某些特定商用(非乘用车)发动机也是强制性的。甚至中国在 2008 年也跟随步伐,到那时,中国的一些轻型车辆被环保局要求实施 OBD。

在大多数汽车上,OBD 接口位于方向盘下方。在 2008 年的丰田 Aygo 上,它位于方向盘下方的右侧。一些汽车制造商没有标准的端口连接。因此,您可能需要购买额外的 OBD 转换电缆。汽车上的端口看起来像这样:

收集汽车数据

汽车上的 OBD 连接

我们将连接一个ELM327-蓝牙发送器到这个 OBD 连接,以及连接到树莓派的上一章中的蓝牙适配器,让这两者进行通信。ELM327 是由ELM Electronics生产的一个用于翻译车载诊断(OBD)接口的程序化微控制器。ELM327 命令协议是最流行的 PC 到 OBD 接口标准之一。你可以在亚马逊上以不同价格购买到具有不同性能的这些硬件。我拥有的这个是由Goliton生产的:

收集汽车数据

ELM 327-OBD 蓝牙发送器

从汽车获取数据的最简单方法是使用安卓上的一个应用程序,它可以为你翻译数据。在 Play 商店搜索 OBD,你会找到很多可以连接到 ELM327 并显示汽车数据细节的优秀应用程序。然而,我们想要比这更有趣。

将汽车数据传输到树莓派

要通过 Python 通过蓝牙从树莓派收集汽车数据,我们需要安装一些工具。运行以下更新命令来下载与蓝牙相关的软件包。请注意,我假设你已经安装了新的 Raspbian。相同的软件包也在之前的章节中安装过:

sudo apt-get install bluetooth bluez-utils blueman python-serial python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev git-core --fix-missing

提示

你很可能现在就坐在车里工作。如果你正在努力想弄清楚如何连接到互联网,你可以随时使用你的安卓设备作为热点,并使用我们稍后在本章中需要的 Wi-Fi 适配器连接到互联网。

将树莓派连接到 Wi-Fi 网络的方法之前已经介绍过了,但让我们再次了解一下它是如何工作的。

将以下行添加到/etc/wpa_supplicant/wpa_supplicant.conf文件中。你需要配置热点以应用 WPA PSK 安全,而不是 PSK2:

network={
        ssid="YOUR_NETWORKID_FOR_HOTSPOT"
        psk="YOUR_PASSWORD_FOR_HOTSPOT"
}

现在,重新启动树莓派,几分钟后,你会发现它自动连接到安卓设备的热点。

再一次,我们可以使用lsusb命令来列出连接的 USB 设备。在我的系统上,输出如下所示:

Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
Bus 001 Device 005: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
Bus 001 Device 006: ID 0a5c:21e8 Broadcom Corp.

005设备是我从上一节中重复使用的蓝牙适配器。发出hcitool scan命令,看看是否可以连接到连接到汽车的 OBD 蓝牙设备:

Scanning ...
 00:1D:A5:15:A0:DC       OBDII

你也可以看到 OBD 设备的 MAC 地址;记下来,稍后会用到。

提示

如果你遇到问题,比如扫描或连接到 OBD,你可以使用以下命令来查看树莓派上连接的蓝牙适配器和bluetooth服务的状态:

hciconfig hci0 
/etc/init.d/bluetooth status 

让我们来看看以下命令:

/etc/init.d/bluetooth restart 

上述命令用于重新启动bluetooth服务。

现在,我们需要让pi用户访问蓝牙设备。编辑/etc/group文件,找到包含bluetooth文本的行,并在该行的末尾添加pi。它需要看起来类似于bluetooth:x:113:pi

现在,使用rfcomm命令将树莓派的蓝牙适配器连接到 OBD 蓝牙设备。在连接到 OBD 之前,这个命令应该是你执行的第一件事。你可以在继续使用Ctrl+C组合键之前挂断:

sudo rfcomm connect hci0 00:1D:A5:15:A0:DC

在这里,你应该使用你自己的 OBD 蓝牙的 MAC 地址,我们之前使用hcitool scan命令找到了它。

现在,发出以下蓝牙配对命令,将树莓派与 OBD 配对,并使用 OBD 的 MAC 地址:

sudo bluez-simple-agent hci0 00:1D:A5:15:A0:DC

PIN 通常是00001234

RequestPinCode (/org/bluez/2336/hci0/dev_00_1D_A5_15_A0_DC)
Enter PIN Code: 1234
Release
New device (/org/bluez/2336/hci0/dev_00_1D_A5_15_A0_DC)

在继续下一个命令之前,我们甚至应该添加dbus连接支持:

sudo update-rc.d -f dbus defaults
sudo reboot

让树莓派信任 OBD 设备,以便下次跳过手动配对,使用以下命令:

sudo bluez-test-device trusted 00:1D:A5:15:A0:DC yes

提示

如果你遇到任何问题,以下命令将让你测试连接。用你的 OBD 适配器的 MAC 地址替换 MAC 地址:

sudo l2ping 00:1D:A5:15:A0:DC

我们将使用一个名为pyOBD-pi的工具来访问 OBD dongle 提供的数据。使用git命令下载并启动记录器。这是一个更开发者友好的版本,位于github.com/peterh/pyobd

git clone https://github.com/Pbartek/pyobd-pi
cd pyobd-pi
sudo python ./obd_recorder.py

提示

不要忘记打开点火开关。还要记得使用即将到来的命令通过蓝牙连接。最好将其放在 crontab 中,否则,您每次重新启动 Pi 时都需要使用它:

sudo rfcomm connect hci0 00:1D:A5:15:A0:DC &

该命令将将数据流量保存到log目录。如果出现关于0100 response:CAN ERROR的错误,则表示您在协议选择方面存在问题,您只需要编辑obd_io.py文件并找到以下行:

self.send_command("0100")

然后,在此之前添加以下代码行:

self.send_command("ATSP0")  # select auto protocol
wx.PostEvent(self._notify_window, DebugEvent([2,"ATSP0 response:" + self.get_result()]))

通过这种方式,我们已经强制选择通信协议。

提示

您可能希望在重新启动时运行init服务器脚本。您不能将其放在 cronbtab 中,因为在运行时蓝牙或 GPS 可能尚未准备就绪。将命令放在/etc/rc.local文件的末尾,而不是在退出行之前:

sudo killall gpsd
sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock
sudo rfcomm connect hci0 00:1D:A5:15:A0:DC &

将您的安卓设备用作访问点

在将迄今为止收集的数据发送到云端之前,我们需要将 Pi 连接到互联网。将安卓设备设置为互联网访问点或热点非常简单,可以从设备的设置中完成。然后,我们可以将 Pi 连接到安卓提供的网络。但是,这种设置存在一个主要问题。首先,我们希望能够一直将 Pi 和手机留在车上。一旦汽车启动,我们希望数据能够自动发送,而不想携带 Pi 和手机。但是,如果我们将手机留在车上并将其连接到 12V 电源输出,设备很快就会耗尽电池并关闭。然后,我们需要手动将其打开并再次更改热点设置。我们希望所有这些步骤都能自动进行。因此,我们需要一种方法,可以在连接到电源源时或连接到的电源源,例如车上的 12V 电源输出,在我们启动汽车时唤醒设备。我现在将介绍的技术要求您对安卓设备拥有超级用户权限,这意味着我们需要对设备进行 Rooting。

Rooting 的替代方法

使用 USB Wi-Fi 3G 调制解调器作为根设备的替代方法,以在汽车中获得互联网访问。请注意,市场上大多数 3G USB 调制解调器都不提供 Wi-Fi 网络。它们只为插入的计算机提供网络访问。我们需要的那种在连接到 USB 电源源时类似于 Wi-Fi 热点。您可以在在线零售商处找到这些产品,如亚马逊或速卖通。我个人使用的产品如下图所示:

Rooting 的替代方法

USB Wi-Fi 3G 调制解调器

如果您选择使用其中一种,可以跳过本节的其余部分,直接转到下一节。

Rooting Samsung Galaxy S2

有不同的方法可以对不同的设备进行 Rooting。我将使用市场上最常见的二手安卓设备之一,即三星 Galaxy S2。如果您有其他手机,互联网上有大量关于如何对每个设备进行 Rooting 的资源。最受欢迎的一个位于www.androidcentral.com/root,即Android Central网站。

注意

请注意,对设备进行 Rooting 将使保修无效。这可能会损坏您的手机,并且不是一个安全的过程。请自担风险。但是这里提供的步骤对我有用。在继续本章的其余部分之前,您应该备份您想要保留的任何文件。

按下音量减电源主页按钮,三星设备可以进入恢复模式。按下这些按钮,您将会看到三星标准的恢复屏幕上有一个警告标志。我们应该用另一个恢复程序替换这个标准恢复程序,因为标准恢复只能通过连接到 USB 的计算机来完成,并下载完整的操作系统映像。然而,我们真正需要做的是只用一个给我们超级用户权限的内核来替换内核。我们还要确保我们是从连接到安卓设备的 SD 卡上进行这个操作。这就是为什么我们需要替换三星的默认恢复程序。我们可以再次使用三星提供的恢复操作来完成这个操作。

当您将设备置于此恢复模式时,通过 USB 将其连接到计算机。接下来,我们可以下载一个名为Odin的软件,将一个新的恢复工具上传到手机上。它可以从互联网上的许多地方下载,有不同的版本。我们将使用的是名为ODIN3_v1.85.zip的版本,它位于www.androidfilehost.com/?fid=9390169635556426736。我们需要的另一个文件是一个内核,用来替换现有的内核,帮助我们进行新的恢复操作。这个文件名为Jeboo Kernel,可以在downloadandroidrom.com/file/GalaxyS2/kernels/JB/jeboo_kernel_i9100_v1-2a.tar找到。

按照手机上恢复屏幕上的指示,按下音量增按钮将设备置于下载模式。然后,启动 Odin,并选择新下载的 Jeboo Kernel 作为PDA。如果手机正确连接并处于内核下载模式,您应该看到一个标记为黄色的 COM 框:

三星 Galaxy S2 获取 Root 权限

Odin 显示 Jeboo 为 PDA,并显示连接到COM11的设备。点击开始上传您选择的新 Jeboo 内核。

在您收到PASS通知之前不应该花费太多时间:

三星 Galaxy S2 获取 Root 权限

Odin 已成功完成

现在,您的手机应该重新启动,您应该在重新启动屏幕上看到一个警告三角形,表示您有一个带有“从 SD 卡恢复”功能的新内核。

下一步是将downloadandroidrom.com/file/tools/SuperSU/CWM-SuperSU-v0.99.zip上的CWM Super User文件保存到 SD 卡并将其连接到设备。现在,关闭设备并再次将其置于恢复模式,这次使用略有不同的按键组合,即音量增电源主页。请注意,我们按下音量增而不是之前的音量减。您将看到一个名为CWM-based Recovery的不同恢复屏幕。您可以使用音量增音量减键上下滚动。使用主页按钮选择安装 Zip项目,然后选择从 SD 卡选择选项。您应该浏览到您已经下载到 SD 卡上的 CWM Super User ZIP 文件。最后,选择

重新启动设备,您将看到一个名为Super User的新应用程序,这表明您已成功获取 Root 权限。您甚至可以通过在 Google Play 上下载一个 Super User 检查器应用程序来验证您对设备的超级用户访问权限。您将看到一个消息框,询问您是否要授予超级用户权限给任何其他应用程序。

连接到电源后启用共享网络

由于我们的手机假设一直停在车上,并且只有在使用车辆时才会上电,因此我们需要找到一种方法,在手机连接到电源时启用 Wi-Fi 共享或热点。但我们可能会遇到两种情况:

  • 电池已耗尽,手机在夜间关闭。在这种情况下,我们需要找到一种方法,以便在手机再次上电时打开手机。这发生在我们启动汽车时。当手机成功开机时,我们需要找到一种方法来启用热点。

  • 手机仍然有足够的电池来保持其开启,但由于未被使用,热点已被禁用。请注意,使用手机热点的唯一设备是 Pi,如果车辆未被使用,则其将被关闭。当我们再次启动汽车时,手机将从 USB 接触点上电。在这种情况下,我们需要再次启用热点。

连接电源时自动重启

当我们将关闭的三星设备连接到电源时,我们将看到一个带有旋转箭头的灰色电池图像。然后,当它开始充电时,我们将看到另一个显示当前充电级别的彩色电池图像。这第二个图像是由一个程序生成的,该程序在关闭的设备开始充电电池时触发。它是手机上/system/bin/playlpm中的一个二进制文件。我们将更改此文件为我们自己的脚本以重新启动设备。为了能够编辑此文件,我们需要超级用户权限。这就是为什么我们对手机进行了 root。由于 Android 系统实际上是一个 Linux 操作系统,我们可以在其下运行任何 Linux 命令。我们可以使用一个可以从 Play 商店下载的应用程序来做到这一点,名为终端模拟器

连接电源时自动重启

终端模拟器应用程序屏幕

现在,发出以下命令来更改playlpm文件的内容并使其成为可执行文件。我们还需要重新挂载/system目录,以便对其进行写操作:

mount -o rw,remount /system
mv playlpm playlpmbackup
echo "#!/system/bin/sh" > playlpm
echo "sleep 60" >> playlpm
echo "/system/bin/reboot" >> playlpm
chmod 0755 /system/bin/playlpm
chown root.shell /system/bin/playlpm
mount -o ro,remount /system

关闭设备并将其连接到电源。您会看到它在一分钟后自动开机。我们引入了一分钟的延迟,因为如果电池完全放电,它将没有足够的容量来重新启动设备。在这种情况下,我们希望至少等待一分钟,让电池充电到足以重新启动设备。如果充电不足,您可能需要在设备可以自动重新启动之前充电手机。您可以将手机放入恢复模式并开始充电,而无需重新启动手机。

自动共享

现在我们可以在连接到电源时重新启动设备。当设备唤醒或连接到电源时,我们还需要启用设备上的共享。市场上已经有应用程序可以做到这一点,但最好的应用程序是收费的。这是我们要为此目的实现我们自己的应用程序的原因之一。另一个原因是这样做很有趣。

我们可以像以前一样在 Android Studio 中创建一个新的应用程序。对于这个应用程序,我们不需要任何Activity

创建一个名为StartTetheringAtBootReceiver的新 java 文件,用于BroadcastReceiver,并在其中添加以下代码:

public class StartTetheringAtBootReceiver extends BroadcastReceiver {
   public static void setWifiTetheringEnabled(boolean enable, Context context) {
      WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);

      Method[] methods = 
         wifiManager.getClass().getDeclaredMethods();
       for (Method method : methods) {
        if (method.getName().equals("setWifiApEnabled")) {
            try {
                 method.invoke(wifiManager, null, enable);
             } catch (Exception ex) {
                ex.printStackTrace();
             }
             break;
         }
      }
   }
    @Override
   public void onReceive(Context context, Intent intent) {
      if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) || Intent.ACTION_POWER_CONNECTED.equals(intent.getAction())) {
         setWifiTetheringEnabled(true, context);
      }
   }
}

这段代码在手机启动或连接到电源时接收广播事件,并使用默认设置在设备上启用共享。如果我们想要更改网络名称或密码,我们需要修改设备上的设置。

将新广播接收器的清单定义添加到AndroidManifest.xml中的application标记内:

<receiver
android:name=".StartTetheringAtBootReceiver"
   android:label="StartTetheringAtBootReceiver">
   <intent-filter>
      <action android:name="android.intent.action.BOOT_COMPLETED" />
      <action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
   </intent-filter>
</receiver>

manifest标记内添加以下权限声明:

<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

现在,将此应用程序安装到您的手机上,并查看每当您重新启动设备或将电源线连接到设备时,共享是否已启用。

我们可以在MainActivity中可选地添加一个用于连接的快捷按钮。在activity_main.xml文件中,添加以下按钮定义:

<Button android:text="@string/enable" 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:onClick="click"/>

接下来,在MainAcitivty.java文件中,为按钮定义处理程序:

public void click(View v) {
   StartTetheringAtBootReceiver
      .setWifiTetheringEnabled(true, this);
}

接下来,我们需要将树莓派连接到我们迄今为止创建的热点。之前已经介绍了将树莓派连接到 Wi-Fi 网络,但让我们再次提醒自己这个概念。将以下代码添加到/etc/wpa_supplicant/wpa_supplicant.conf文件中。我们可以配置热点应用 WPA PSK 安全而不是 PSK2:

network={
        ssid="YOUR_NETWORKID_FOR_HOTSPOT"
        psk="YOUR_PASSWORD_FOR_HOTSPOT"
}

现在,我们将重新启动树莓派,几分钟后,我们将看到它自动连接到 Android 设备的热点,在热点设置窗口中:

自动连接

在 Android 的热点设置中显示已连接设备的列表

你一定会想知道为什么我们在这个时候涵盖了这个内容。这是因为为了实现下一节,你很可能需要坐在你的车里,与你车内的树莓派进行通信,在那里你很可能没有比 Android 提供的热点更多的网络访问。现在,如果你从电脑上连接到 Android 上的同一个热点,你将能够使用一个叫做PuTTY的工具在 Windows 机器上安装,或者在 Mac 上使用内置的 SSH 终端工具来 SSH 到树莓派。

将数据发送到云端

我们将使用 Google Docs 电子表格来保存数据,并使用专门为此目的开发的 Python 库。我们首先通过创建一个 API 密钥来访问 Google 服务来实现这一点。

浏览console.developers.google.com/project并为此目的创建一个帐户。当准备好时,您将被引导到 Google Developer Console:

将数据发送到云端

Google Developer Console 的起始页面

在这里,我们需要在选择项目下拉菜单中创建一个新项目。给它一个合适的名字,接受协议,然后点击创建。选择新创建的项目,APIs & auth,然后从左侧菜单中选择APIs。然后,找到并选择Drive API,点击启用 API按钮。当它被启用后,转到APIs & auth下的左侧菜单中的凭据

将数据发送到云端

在 Google Developer Console 中启用 OAuth 的菜单

在这里,在OAuth下,点击创建新的客户端 ID按钮。在出现的消息框中,选择服务帐户,然后点击创建客户端 ID按钮。我们将看到一个框,告诉我们我们已成功为项目创建了新的公共/私有密钥对。我们甚至会看到网站已经向我们发送了一个带有凭据的JSON文件。对于我创建的虚拟帐户,内容看起来类似于这样:

{
  "private_key_id": "ed5a741ff85f235167015d99a1adc3033f0e6f9f",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDM9YJ2otxwdhcL\nQJ8ipZOuILkq9dzWDJJgtjSgFUXTJvjgzTDNa2WXGy9p9i4Wuzrj5OJli/M5dMWr\n+CVZCpsfV7Xt7iqkeCEo0dN225HDiAXXMvWKhDsiofau0xLCTFLDnLZFWqAd55ec\naENYQKp6ZEc6dGaA7Kp7O1+7LtEB2a4yqgZIelL6fTSSLQqyV477OS2Dkq+nz5Sz\nRyTexcDWioDNp2vdGadqDfRKsI7ELwgWscaV6jrbHz2uDuC844UnTL4WKMugp1n1\nObTuGDl1gldEIWlk2XSLFkGfY30lYV7XwrUQGgc85AGRwdH7qYrQM3jO4D+6thAH\nETq4qjRRAgMBAAECggEAJjXXHrr6EdVSMnzXriPkRmA/ZSz1AMrTN0iAwx90Jwtq\n9q4KXSGajPM6gaytpvs83WO8eWX/8EQ+3fKjM9hwVwWJG1R9irACrpN/svb4U9W2\nEQqlEC/avngnfyxGoQaNn35F1OQyWaDlePlPJNLZdXvgc5tjyMFWfybwj/sIaCmR\nj5ntV2aY/gCEbe6km7L/LkC3C7CesIWstUGMHCjh2aPeQT+Hpodf23AnLZuSo34j\nB+lSI/RjnDsd0HfazOgaOXa/yK4SliTaMWUBiMSXQcwZZsVp/RL0Ve6W2PSfi092\n+hATaaRnA8zB8fx7PnAltPhFwVr9+jjbYbq+wypoyQKBgQDoLJytaR4wof46MUiL\nMWrXDopi5dG2ofUSXR+JEIThe7yyYepzzdWFL+rXNEzD5X9UcfCodwZ0PKLN3u0t\nZJ5Iq111bxwwZix5uVStRi6stgGaewF6nkDqN8y5TJJgnZB9wSBuG3RvCU4zwXKZ\ngj2+azWme7PSyOHKNODbBd9DkwKBgQDh/e7nct49/Z0Om/+kNJ+NXUjka+S1yF7n\nhL+HZ2WU1gL8iQjXPxnCX1lThw7C4rForH/esOs+f1XMje8NYi7ggslqxoXwFRH6\ny/tuCRaY+e62xmJAxj2o8InsvQQkSM+dtuZiaNq3gCatHKbx2C6SVQal/y3yuR0c\n00adgr6fCwKBgQDSlAvzGIFiWLsNqr+CR+sAbVbExm9EN3bhFgdROONc4+7M2BRe\nvlUoPMLCN9RcZR3syH8fPP1klc6P7N6vqjAJ9yuIJKOrnjA+owKTOjGBQn8HzwMT\nZM+536xWcIXfDWoNNQol887SGt2MAavgYYmA2RpLCq2Zw8tOrFE5NgU+8wKBgQCe\nAiwNy3S0JySu2EevidOcxYJ3ozBwIT6p5Vj81UBjBhdkdnOl+8qI6p3MFvwtKs8b\n/rARBeYU9ncI5Jwl4WYhN5CYhWGUcUb28bRERTp1jxpm1OJRo8ns2vG0gpvourfe\n78i5OdLixklEdGoNYjd9vNE/MuHveZpvUxFmg8m/7QKBgCGVTkOXWLpRxuYT+M+M\n28LBgftHxu0YZdXx8mU9x6LQYG2aFxho7bkEYiEaNYJn51kdNZqzrIHebxT/dh/z\nddd5nR93E6WsPuqstZF4ZhJ+l2m77wmG9u5gfRifrNpc3TK0IswydFPIMNVxMz+d\nl3cdqtiW6rvWSQoHC0brpcYL\n-----END PRIVATE KEY-----\n",
  "client_email": "14902682557-05eecriag0m9jbo50ohnt59sest5694d@developer.gserviceaccount.com",
  "client_id": "14902682557-05eecriag0m9jbo50ohnt59sest5694d.apps.googleusercontent.com",
  "type": "service_account"
}

我们可以选择使用 Developer Console 网站上的生成新的 JSON 密钥按钮为我们的项目生成新的密钥。

在这个阶段,我们需要使用生成新的 P12 密钥按钮生成一个P12密钥。这个文件将在以后使用。当我们下载文件时,还会提供一个秘钥,我们需要记下来。下面的截图展示了成功创建 API 密钥后的 Google Developer Console:

将数据发送到云端

成功创建 API 密钥后的 Google Developer Console

在我们安装 Google Python 库之前,我们需要安装一个叫做pip的工具,它将帮助我们安装一个 OAuth 客户端,我们将用它来连接到 Google 服务。使用以下命令来完成这个过程:

curl -O https://raw.githubusercontent.com/pypa/pip/master/contrib/get-pip.py
sudo python get-pip.py

然后,使用这个新的pip工具来安装 OAuth 客户端:

sudo apt-get update
sudo apt-get install build-essential libssl-dev libffi-dev python-dev
sudo pip install --upgrade oauth2client
sudo pip install PyOpenSSL

下一步是下载并安装客户端库,以使用以下命令在树莓派上访问 Google Sheets:

git clone https://github.com/burnash/gspread.git
cd gspread
sudo python setup.py install

在开始编码之前,我们需要在docs.google.com网站上添加一个新的电子表格。在菜单中选择表格,使用加号(+)号创建一个新表格,并将名称从Untitled spreadsheet更改为CAR_OBD_SHEET。它应该会自动保存。我们需要与我们生成 OAuth 密钥对时为我们创建的 Google 开发者控制台客户端共享此电子表格。我们将在我们下载的 JSON 文件中找到一个client_email字段。我们将与此客户端共享新的电子表格。现在,在 Google Docs 中打开CAR_OBD_SHEET电子表格,并单击共享按钮:

将数据发送到云端

在 Google Docs 中打开电子表格

在弹出窗口中,粘贴 JSON 文件中的client_email,然后单击弹出窗口上的发送按钮。这将与在上一步创建 OAuth 密钥对时生成的客户端共享电子表格:

将数据发送到云端

与生成的客户端共享电子表格

现在,我们将测试一下是否一切正常。在 Pi 上创建一个文件,命名为send_to_sheet.py,并将以下内容放入其中。不要忘记创建 OAuth JSON 文件,并将我们从 Google 开发者控制台下载的内容放入其中,并将其命名为piandroidprojects.json

import json
import gspread
from datetime import datetime
from oauth2client.client import SignedJwtAssertionCredentials

json_key = json.load(open('piandroidprojects.json'))
scope = ['https://spreadsheets.google.com/feeds']

credentials = SignedJwtAssertionCredentials(json_key['client_email'], json_key['private_key'], scope)

gc = gspread.authorize(credentials)
t = datetime.now()
sh = gc.open("CAR_OBD_SHEET").add_worksheet(str(t.year) + "_" + str(t.month) + "_" + str(t.day) + "_" + str(t.hour) + "_" + str(t.minute) + "_" + str(t.second), 100, 20)

sh.update_cell(1, 1, 0.23)

现在,使用python send_to_sheet.py命令运行该文件,我们将在 Google Docs 表格上看到更新。该代码将创建一个名为当前时间戳的新工作表,并在该工作表中保存一个单个值。请注意,Google 允许每个表格 200 个工作表,默认情况下每个工作表 100 行;在我们的代码中,每次运行时都会创建一个新的工作表。我们需要定期清理工作表,以免超出限制。

将所有内容放在一起

在接下来的两个部分中,我们将总结到目前为止所做的工作。首先,我们将开始将数据发送到 Google Docs 表格。然后,我们将构建一个 Android 应用程序来在地图上显示数据。

发送测量数据

我们将使用一个 Python 脚本来访问 Pi 上的 GPS 数据,我们需要在系统重新启动时运行该脚本。为此,在/etc/rc.local文件的末尾添加以下代码:

sudo killall gpsd
sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock
sudo rfcomm connect hci0 00:1D:A5:15:A0:DC &
sleep 1m
current_time=$(date "+%Y.%m.%d-%H.%M.%S")
file_name=/home/pi/log_sender.txt
new_filename=$file_name.$current_time
sudo /home/pi/pyobd-pi/sender.py > $new_filename 2>&1 &

在这里,我们可以重新启动 GPS 服务,连接到 OBD 蓝牙适配器,创建日志文件,并启动我们将在下一步实现的sender.py脚本:

#!/usr/bin/env python

import obd_io
from datetime import datetime
import time
import threading
import commands
import time
from gps import *
import math
import json
import gspread
from oauth2client.client import SignedJwtAssertionCredentials

gpsd = None

class GpsPoller(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    global gpsd 
    gpsd = gps(mode=WATCH_ENABLE) 

  def run(self):
    global gpsd
    while True:
      gpsd.next()

class OBD_Sender():
    def __init__(self):
        self.port = None
        self.sensorlist = [3,4,5,12,13,31,32]

    def connect(self):
        self.port = obd_io.OBDPort("/dev/rfcomm0", None, 2, 2)
        if(self.port):
            print "Connected to "+str(self.port)

    def is_connected(self):
        return self.port

    def get_data(self):
        if(self.port is None):
            return None
        current = 1
        while 1:
            cell_list = []

            localtime = datetime.now()
            cell = sh.cell(current, 1)
            cell.value = localtime
            cell_list.append(cell)

            try:
                gpsd.next()
            except:
                print "gpsd.next() error"

            cell = sh.cell(current, 2)
            cell.value = gpsd.fix.latitude
            cell_list.append(cell)

            cell = sh.cell(current, 3)
            cell.value = gpsd.fix.longitude
            cell_list.append(cell)

            column = 4
            for index in self.sensorlist:
                (name, value, unit) = self.port.sensor(index)
                cell = sh.cell(current, column)
                cell.value = value
                cell_list.append(cell)
                column = column + 1

            try:
                sh.update_cells(cell_list)
                print "sent data"
            except:
                print "update_cells error"

            current = current + 1
            time.sleep(10)

json_key = json.load(open('/home/pi/pyobd-pi/piandroidprojects.json'))
scope = ['https://spreadsheets.google.com/feeds']

credentials = SignedJwtAssertionCredentials(json_key['client_email'], json_key['private_key'], scope)

while True:
    try:
        gc = gspread.authorize(credentials)
        break
    except:
        print "Error in GoogleDocs authorize"

t = datetime.now()
sh = gc.open("CAR_OBD_SHEET").add_worksheet(str(t.year)+"_"+str(t.month)+"_"+str(t.day)+"_"+str(t.hour)+"_"+str(t.minute)+"_"+str(t.second), 100, 20)

gpsp = GpsPoller()
gpsp.start()

o = OBD_Sender()
o.connect()
time.sleep(5)
o.connect()
time.sleep(5)
o.get_data()

代码从定义json_key的末尾开始运行,加载 JSON 密钥文件。然后,我们将尝试使用gspread.authorize(credentials)方法进行授权。下一步是创建一个以日期时间戳为标题的新工作表,然后在由GpsPoller类定义的另一个线程中开始消耗 GPS 数据。接下来,我们将初始化OBD_Sender类并两次连接到 OBD 蓝牙设备。当执行连接操作时,第一次可能会失败,但第二次几乎总是成功。然后,我们需要运行OBD_Sender类的get_data方法来开始循环。

GpsPoller类消耗了连接到串行 USB 端口的 GPS 设备的所有值。这是为了在访问gpsd.fix.latitudegpsd.fix.longitude变量时获得最新的值。

OBD_Sender类的get_data方法将本地时间、纬度和经度值发送到电子表格,并且还发送了在self.sensorlist = [3,4,5,12,13,31,32]中定义的七个不同的读数。我们可以在obd_sensors.py文件的SENSORS列表中看到这些值。供您参考,这些是燃油系统状态、计算负荷值、冷却液温度、发动机转速、车速、发动机启动分钟和发动机运行 MIL 值。我们可以更改索引以读取我们想要的值。在en.wikipedia.org/wiki/OBD-II_PIDs上查看其他值。我们遍历这些代码,读取它们的当前值,并将它们发送到工作表的当前行的不同单元格中。启动并驾驶您的汽车后,您会看到数据上传到电子表格,如下面的屏幕截图所示:

发送测量值

数据上传到电子表格

检索测量值

我们将构建我们自己的应用程序来下载测量值并在地图上显示它们。在 Android Studio 中创建一个新的空白项目,并在创建项目向导的最后一步选择包含 Google 地图活动。我将 Android 4.3 作为此项目的基本 SDK;我将把我的主要活动命名为MapsActivity

为了访问 Google 文档并下载电子表格的内容,我们将使用 Google 提供的一些 Java 库。它们位于不同的位置。从以下位置下载 ZIP 文件:

现在,打开这些 ZIP 文件,找到以下 JAR 库,并将它们移动到 Android app目录下的libs文件夹中:

  • gdata-base-1.0.jar

  • gdata-core-1.0.jar

  • gdata-spreadsheet-3.0.jar

  • google-api-client-1.20.0.jar

  • google-http-client-1.20.0.jar

  • google-http-client-jackson-1.20.0.jar

  • google-oauth-client-1.20.0.jar

  • guava-11.0.2.jar

  • jackson-core-asl-1.9.11.jar

要将这些库包含在您的 Android 项目中,您需要将它们添加到build.gradle文件的Module:app下。为此,请在dependencies标签下添加以下代码。

compile files('libs/gdata-spreadsheet-3.0.jar')
compile files('libs/gdata-core-1.0.jar')
compile files('libs/guava-11.0.2.jar')
compile files('libs/gdata-base-1.0.jar')
compile files('libs/google-http-client-1.20.0.jar')
compile files('libs/google-http-client-jackson-1.20.0.jar')
compile files('libs/google-api-client-1.20.0.jar')
compile files('libs/google-oauth-client-1.20.0.jar')
compile files('libs/jackson-core-asl-1.9.11.jar')

当您编辑build.gradle文件时,您可能会在 Android 中收到一条消息,指出Gradle 文件自上次项目同步以来已更改。可能需要进行项目同步以使 IDE 正常工作。单击附近的立即同步链接以更新项目。

下一步是移动我们从 Google 开发者控制台下载的P12密钥文件,并将其包含在我们的 Android 项目中。我们需要将此文件复制到位于PROJECT_HOME\app\src\main\res\rawraw目录中,并将其重命名为piandroidprojects.p12

由于我们计划在地图上显示内容,因此我们将使用 Google 的地图 API。为了使用它,我们需要一个访问 API 密钥。再次转到开发者控制台 console.developers.google.com/project,并选择我们之前创建的项目。在左侧的菜单中,选择 APIs 下的 APIS & auth,然后选择 Google Maps Android API,最后,单击 启用 API 按钮。接下来,转到 凭据,并在 公共 API 访问 部分下单击 创建新密钥 按钮。我们需要在弹出的窗口中选择 Android 密钥。复制生成的 API 密钥,并将其替换为 google_maps_api.xml 文件中的 YOUR_KEY_HERE 字符串。现在,我们已经准备好了我们的 Android 项目设置,现在是编写代码的时候了。

代码中要做的第一件事是从 Google Docs 下载工作表列表。每次 Pi 重新启动时都会有一个工作表。将以下代码添加到 MapsActivity.java 文件的 onCreate 方法中:

new RetrieveSpreadsheets().execute();

这段代码将创建一个异步任务,该任务实现为 Android 的 AsyncTask,用于下载和显示电子表格。让我们在同一文件中定义任务类:

class RetrieveSpreadsheets extends AsyncTask<Void, Void, List<WorksheetEntry>> {
   @Override
   protected List<WorksheetEntry> doInBackground(Void params) {
      try {
         service = 
            new SpreadsheetService("MySpreadsheetIntegration-v1");
         HttpTransport httpTransport = new NetHttpTransport();
         JacksonFactory jsonFactory = new JacksonFactory();
         String[] SCOPESArray = 
            {"https://spreadsheets.google.com/feeds", 
             "https://spreadsheets.google.com/feeds/spreadsheets/private/full", 
             "https://docs.google.com/feeds"};
         final List SCOPES = Arrays.asList(SCOPESArray);
         KeyStore keystore = KeyStore.getInstance("PKCS12");
         keystore.load(
            getResources().openRawResource(R.raw.piandroidprojects), "notasecret".toCharArray());
         PrivateKey key = (PrivateKey) keystore.getKey("privatekey", "notasecret".toCharArray());

         GoogleCredential credential = 
            new GoogleCredential.Builder()
                .setTransport(httpTransport)
                .setJsonFactory(jsonFactory)
                .setServiceAccountPrivateKey(key)
                .setServiceAccountId("14902682557-05eecriag0m9jbo50ohnt59sest5694d@developer.gserviceaccount.com")
                .setServiceAccountScopes(SCOPES)
                .build();

         service.setOAuth2Credentials(credential);
         URL SPREADSHEET_FEED_URL = new URL("https://spreadsheets.google.com/feeds/spreadsheets/private/full");
         SpreadsheetFeed feed = 
            service.getFeed(SPREADSHEET_FEED_URL, SpreadsheetFeed.class);
         List<SpreadsheetEntry> spreadsheets = feed.getEntries();

         return spreadsheets.get(0).getWorksheets();

      } catch (MalformedURLException e) {
         e.printStackTrace();
      } catch (ServiceException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      } catch (GeneralSecurityException e) {
         e.printStackTrace();
      }
      return null;
   }

   protected void onPostExecute(final List<WorksheetEntry> worksheets) {
      if(worksheets == null || worksheets.size() == 0) {
         Toast.makeText(MapsActivity.this, "Nothing saved yet", Toast.LENGTH_LONG).show();
      } else {
         final List<String> worksheetTitles = 
            new ArrayList<String>();
         for(WorksheetEntry worksheet : worksheets) {
             worksheetTitles.add(
                worksheet.getTitle().getPlainText());
         }

         AlertDialog.Builder alertDialogBuilder = 
            new AlertDialog.Builder(MapsActivity.this);
         alertDialogBuilder.setTitle("Select a worksheet");
         alertDialogBuilder.setAdapter(
            new ArrayAdapter<String>(
                MapsActivity.this,
                android.R.layout.simple_list_item_1, worksheetTitles.toArray(new String[0])),
                new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                      new RetrieveWorksheetContent()
                         .execute(worksheets.get(which));
                   }
             });
            alertDialogBuilder.create().show();
}
      }
   }

在我们描述上述代码之前,为电子表格服务定义一个实例变量,该变量在我们刚刚定义的任务中使用:

SpreadsheetService service;

Android 的 AsyncTask 要求我们重写 doInBackground 方法,每当我们调用 onCreate 中执行的任务的 execute 方法时,它就会在新线程中执行。在 doInBackground 中,我们将定义 KeyStore,并加载我们从 Google Developer Console 下载并复制到 Android 项目的 raw 目录中的 P12 文件。请注意,notasecret 是开发者控制台在我创建和下载 P12 文件时通知我的秘密。此外,在 setServiceAccountId 方法内,您需要使用自己的帐户名。您可以在开发者控制台的 服务帐户 部分的 电子邮件地址 字段以及 JSON 密钥文件的 client_email 字段中找到它。在加载密钥文件并定义凭据之后,我们将使用 OAuth 授权自己访问 Google 电子表格服务。我们将简单地获取我假设是 CAR_OBD_SHEET 的第一个电子表格,并返回其中的工作表。我们也可以遍历所有电子表格并搜索标题,但我将跳过此部分代码,并假设您的帐户中只有一个标题为 CAR_OBD_SHEET 的电子表格。

我们将定义的第二个函数是 onPostExecute。每当后台处理时,此函数由 Android 系统在 UI 线程中调用。重要的是,这在 UI 线程中运行,因为如果我们在非 UI 线程中运行与 UI 相关的代码,就无法触摸 UI 元素。

请注意,doInBackground 方法的返回值作为参数发送到 onPostExecute 方法,这是在 Google Docs 服务中找到的工作表的列表。我们将遍历此列表并将标题收集到另一个列表中。然后,我们将在弹出对话框中显示此列表,用户可以单击并选择。每当用户选择工作表之一时,Android 将调用 DialogInterface.OnClickListeneronClick 方法,我们已将其作为参数发送到 AlertDialog 的适配器中。此方法调用另一个名为 RetrieveWorksheetContentAsyncTaskexecute 方法,正如名称所示,它检索所选工作表的内容。以下是此任务的定义:

class RetrieveWorksheetContent extends AsyncTask<WorksheetEntry, Void, List<List<Object>>> {

   @Override
   protected List<List<Object>> doInBackground(WorksheetEntry params) {
      WorksheetEntry worksheetEntry = params[0];
      URL listFeedUrl= worksheetEntry.getListFeedUrl();
      List<List<Object>> values = new ArrayList<List<Object>>();
      try {
         ListFeed feed = 
            service.getFeed(listFeedUrl, ListFeed.class);
         for(ListEntry entry : feed.getEntries()) {
             List<Object> rowValues = new ArrayList<Object>();
             for (String tag : entry.getCustomElements().getTags()) {
               Object value = 
                  entry.getCustomElements().getValue(tag);
                rowValues.add(value);
            }
            values.add(rowValues);
         }
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ServiceException e) {
         e.printStackTrace();
      }
      return values;
   }

   @Override
   protected void onPostExecute(List<List<Object>> values) {
      setUpMap(values);
      super.onPostExecute(values);
   }
}

在这里,最重要的部分是我们遍历feed.getEntries(),它指的是电子表格中的所有行,以及我们遍历entry.getCustomElements().getTags()的部分,它指的是所有的列。然后,在onPostExecute中,我们将使用我们检索到的所有值调用setUpMap方法。在这个方法内部,我们将在MapsActivity中包含的地图上创建标记。如果您不希望在位置0,0处有一个标记,可以注释掉 Android Studio 为您自动定义的setUpMap方法作为示例:

private void setUpMap(List<List<Object>> values) {
   for(List<Object> value : values) {
       String title = values.get(0).toString();
       try {
         double latitude = 
            Double.parseDouble(value.get(1).toString());
         double longitude = 
            Double.parseDouble(value.get(2).toString());
         if (latitude != 0 && longitude != 0)
             mMap.addMarker(
               new MarkerOptions().position(
                   new LatLng(latitude, longitude)))
                .setTitle(title);
      } catch(NumberFormatException ex) {
      }
   }
}

当您启动应用程序时,您将看到一个可供选择的电子表格列表:

检索测量数据

电子表格列表

接下来,在选择这些表格中的一个之后,您可以在地图上看到数据:

检索测量数据

地图上的数据点

总结

在本章中,我们涵盖了很多内容,从汽车诊断到 Android 设备的 root 过程。我们甚至涵盖了大量的 Android 代码。

我希望你们所有人都能乐在其中,实现这些令人兴奋的项目,会尝试改进它们,使它们比我做得更好。

posted @ 2024-05-22 15:15  绝不原创的飞龙  阅读(26)  评论(0编辑  收藏  举报