65.9K
CodeProject 正在变化。 阅读更多。
Home

如何将手机连接到 Raspberry Pi

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (4投票s)

2017 年 2 月 4 日

CPOL

9分钟阅读

viewsIcon

39673

downloadIcon

382

如何设置一个远程浏览器设备与 Raspberry Pi 之间的双向接口。

引言

本文介绍如何设置支持浏览器的远程设备(如手机)与 Raspberry Pi 之间的双向接口。示例应用程序是一个简单的家庭环境控制器。Pi 将温度信息发送到订阅者的浏览器,并通过打开或关闭远程无线电控制的插座的请求进行响应。它是用 node.js 编写的。

View

配置 Pi

该项目使用运行最新版本 Raspbian 的 Raspberry Pi 3,并启用了互联网 Wi-Fi。您可以使用早期版本的 Pi,但端口引脚号可能与此处提到的不对应。Pi 需要启用一些接口连接。这曾经是一项漫长而乏味的工作,但现在已经大大简化了。最简单的方法是将 Pi 的 HDMI 线连接到显示器,连接一根已连接互联网的以太网线、一个 USB 键盘和鼠标,然后启动到 Raspbian。在屏幕左上角的树莓图标中,选择首选项/树莓派配置/接口,然后启用 SSH、1-Wire 和远程 GPIO,然后关闭弹出窗口。要启用 Wi-Fi,请单击屏幕右上角的向上/向下箭头图标并按照提示操作。移除以太网连接,然后单击命令窗口图标并输入命令 ‘hostname –I’。记下显示的 IP 地址,您需要它来连接到 Pi。然后重启 Pi (输入 ‘reboot’)。

安装和更新软件

需要安装几个软件包。第一个是 Node.js 包管理器 npm。它管理应用程序所需的包。它使用 package.json 文件来维护、加载和更新所需的包。第二个是 Mocha。Mocha 是一个有用的 node.js 测试框架,它有助于构建和运行测试。最后,预装的软件应更新到最新版本的 Pi 兼容版本。做到这一点的最好方法是更新包列表,然后升级到更新包列表中详细列出的最新版本。如果安装在显示一页安装日志详细信息后停止,请输入 ‘Ctrl V’ 继续,然后在出现提示时输入 ‘q’ 返回输入终端。不建议直接从提供商的网站下载,因为可能存在不兼容问题 - 某些包是 Pi 特定的。以下是需要输入的命令。

sudo apt-get update
sudo apt-get upgrade
sudo apt install npm
sudo npm install --global mocha

设置服务器

服务器需要响应来自远程浏览器的 GETPUT 请求。GET 请求仅上传 HTML 网页。PUT 请求在网页上的按钮被按下时发送。PUT 请求的 URL 的最后一个单词与按钮的名称相关。Pi 的 GPIO 端口与插座的远程控制器接口连接,以便在特定端口引脚的电压水平响应按钮按下而改变时,远程控制器会向相应的插座发送无线电脉冲,将其切换为打开或关闭。

app.get('/', function (req, res) {
    res.sendFile(__dirname + '/Views/home.html');

});

// '/:pinName' traps  post requests to /alarm, /tv etc
//and places the last word in a req parameter 'pinName'
app.post('/:pinName', function (req, res) {
    var pinName = req.params.pinName;
    var portNumber = portDictionary[pinName];
    if (portNumber) {
        //The pin is pulsed asynchronously
        //but only one pin can be pulsed at a time
        //because the remote control can only send one
        //pulse at a time. So calls to this are queued.
        gpio.queuePulse(portNumber, pulseLength);
        //code 204 = No content
        res.status(204).end();
    }
    else {
        console.log("port requested is not found ");
        res.status(404).end();
    }
});

更新网页

网页上显示的温度每 5 秒更新一次。这是通过利用 Server Sent Events (SSE) 实现的。客户端通过向 SSE 端点发送 GET 请求来订阅事件。服务器通过与客户端建立开放连接并开始发送包含更新温度的消息包来响应。客户端解开消息并显示更新的信息。当客户端关闭网页时,请求的 Close 事件会触发,服务器会取消订阅客户端。如果连接意外终止,客户端将在大约三秒后自动尝试重新连接。

//this is the server sent event socket
//the client calls this to both subscribe and unsubscribe
app.get('/sse', function (req, res) {
    if (req.headers.accept && req.headers.accept == 'text/event-stream') {
        //the following runs synchronously
        //when the client's close event is emitted (fired)
        req.on('close', function () {
            sse.removeSubscriber(res);
            res = null;
        });
        if (res != null) {
            sse.addSubscriber(req, res);
        }

    } else {
        res.writeHead(404);
        res.end();
    }
});

addSubcriber 函数将订阅者的响应对象添加到数组中,并在必要时启动消息泵。响应对象实际上是一个流,对于给定的连接有一个恒定的引用值,因此可以存储和重用。消息之间需要用一对换行符分隔。

function addSubscriber(req, res) {
  subscribers.push(res);
  res.writeHead(200,
    {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  );
  //send new client's first message  immediately
  writeMessage(res);
  //if cancellationToken is null or undefined
  if (!cancellationToken) {
    //The message pump is not running so
    //start it.  The timer's interval  function
    //runs until cancelled
    cancellationToken = setInterval(function () {
      messageAllSubscribers();
    }, sendInterval);
  }
}

function messageAllSubscribers() {
  subscribers.forEach(function (subscriberRes) {
    writeMessage(subscriberRes);
  });
}

function writeMessage(res) {
  sensor.get(sensorId, function (err, temp) {
    if (err) {
      console.log(err);
      return;
    }
    res.write("data: " + temp + '\n\n');
  });
}

// The response object is actually a stream and has
// a constant reference value for a given connection.
// So it can be stored and reused.
function removeSubscriber(subscriberRes) {
  var i = subscribers.indexOf(subscriberRes);
  //if found
  if (i !== -1) {
    //remove from array
    subscribers.splice(i, 1);
  }
  //if no subscribers remain
  if (subscribers.length == 0 && cancellationToken) {
    //clear (stop) the interval timer
    clearInterval(cancellationToken);
    cancellationToken = null;
  }
}

客户端代码

在 HTML 文档中,只需几行 JavaScript 即可处理服务器发送的事件。

if(typeof(EventSource) !== "undefined") {
    var source = new EventSource("./sse");
    source.onmessage = function(event) {
        document.getElementById("result").innerHTML ="Temperature "+ event.data+" °C" ;
    };
} else {
    document.getElementById("result").innerHTML = "Unable to update the temperature";
}

还有一个将 POST 请求发送到服务器的函数。按钮单击事件使用设置为输入参数的按钮 ID 来调用它。

function postRequest(id) {
var x=  document.getElementById(id);
x.disabled = true;
//disable the button for a short time to avoid spurious posts
setTimeout(function(){ x.disabled=false }, 750);
var xhr = new XMLHttpRequest();
xhr.open('POST', "./"+id, true);
xhr.timeout = 2500; // time in milliseconds
xhr.send();
xhr.ontimeout = function (e) {
  // XMLHttpRequest timed out.
 alert("Request timed out");
};
}

与 Pi 的 GPIO 端口接口

通用输入输出端口 (GPIO) 使用出色的 wiring-pi 包进行控制。

var wpi = require('wiring-pi');
//Associate pin numbers with their
//physical position on the chip
wpi.setup('phys');

function setGpioPins(portDictionary) {
  for (var key in portDictionary) {
    //configure all pins as outputs
    wpi.pinMode(portDictionary[key], wpi.OUTPUT);
  }
}

function pulsePin(pin, delay,cb) {
  wpi.digitalWrite(pin, 1);
  setTimeout(function () {
    wpi.digitalWrite(pin, 0);
    cb();
  }, delay);
}

限制输出引脚的请求

插座的遥控器一次只能切换一个插座,因此一次只能脉冲一个引脚很重要。这是通过使用同步队列来实现的,其中任务按顺序执行。当一个异步任务完成时,另一个任务开始。在这种情况下,任务是引脚的脉冲。

module.exports = TaskQueue;
var taskWorker;
var queue = [];

function TaskQueue(worker) {
    taskWorker = worker;
    }

var processTask = function () {
    var taskParams = queue[0];
     taskWorker(taskParams, dequeue);
}
//This is passed to the taskWorker as
//its callback function.
var dequeue = function () {
    queue.splice(0, 1);
    if (queue.length > 0) {
        processTask();
    }
}

TaskQueue.prototype.queueTask = function (taskParams) {
    //Add task to queue
    var length = queue.push(taskParams);
    //if queue was empty
    if (length == 1) {
        processTask();
    }
    //else, wait for the current task to call dequeue
    //when it completes
}

读取温度传感器

使用的传感器是 DS18B20。它通过单数据线将信息作为包发送,该数据线连接到 Pi 上的物理引脚 7。每个传感器都有唯一的 ID,可以有多个传感器共享一个端口。输入引脚应将其内部电阻设置为“上拉”。这会导致在引脚未驱动时引脚变为高电平。

用于读取传感器的包是 ds18x20。它非常易于使用。

var sensor = require('ds18x20');
//***need to add your own sensor's Id****
// The Id of the sensor can be found in  /sys/bus/w1/devices/ 
// The Id will start with  28- as in the example below
var sensorId = '28-000007d4684f';
function writeMessage(res) {
  sensor.get(sensorId, function (err, temp) {
    if (err) {
      console.log(err);
      return;
    }
    res.write("data: " + temp + '\n\n');
  });
}

与遥控器接口

这有点离题,因为它更多的是关于电子学而不是编码,所以我将细节放在示例应用程序的 README.html 文件中。基本上,发生的事情是 Pi 的输出引脚连接到一个简单的晶体管开关。当引脚变高时,晶体管导通并短接通常由遥控器上的按钮按下连接的相同触点。由于开关仅通过几毫安电流,因此不需要昂贵的继电器。

测试

我首选的方法是在 PC 上进行大部分开发和测试工作,然后将结果文件传输到 Pi。为了做到这一点,您需要下载以下内容

  1. Telnet 客户端,例如 PuTTY,以便 PC 可以充当 Pi 的远程终端
  2. 文件传输实用程序,例如 WinSCP。用于在 Pi 和 PC 之间传输和同步文件
  3. 代码编辑和调试平台。Visual Studio Code (vscode)。非常棒。

在 PC 上测试时,最好使用直接与 pi 固件接口的模块的模拟版本。尝试在 PC 上安装 wiring-pi 会失败。示例应用程序设置为在调试模式下运行,并将使用 'wiring-pi' 和 'ds18x20' 的模拟版本。要在 Pi 上直接运行它,请将 config.json 中的 "NODE_ENV" 值从 'debug' 更改为 'production'。

有用硬件

Pi 的绝缘外壳被认为是必不可少的,除非您当然希望体验蓝烟告别显示。另一件值得拥有的设备是突破性排线和连接器,称为“cobbler”,用于将 Pi 端口连接到原型 面包板。选择完全组装好的版本。自己焊接引脚不值得,一个“虚焊”的引脚会比没用还糟。一个非常有用的测试仪器是逻辑探头。它们很便宜,可以提供任何端口引脚逻辑状态(高或低)的视听指示。它们还可以检测电压脉冲。您可以使用万用表代替,但在数字电路中,逻辑状态比实际电压读数更重要。

将 Raspberry Pi 添加到网络

为了从远程机器调试 Pi,您需要将 Pi 添加到共享网络。为此,请安装一个名为 Samba 的实用程序,然后备份其 samba.conf 文件并按照下面的详细说明编辑原始文件。要使用的命令是

sudo apt-get install samba samba-common-bin
cd /etc/samba
sudo cp smb.conf smb.backup
sudo nano smb.conf 

[global] 部分

 workgroup = WORKGROUP
 wins support = yes 

#=====共享定义========= 部分
[homes]

readonly= yes 

最后,将以下内容复制并粘贴到 [printers] 部分正上方的空白行上

[piPublic]
  comment= Pi Public
  path=/home/pi/Public
  browserable=Yes
  writeable=Yes
  guest ok=Yes
  create mask=0775
  directory mask=0775
  public=yes

保存文件,然后将密码设置为用户 'pi' 登录时使用的密码

smbpasswd -a pi

然后重启。您应该在网络上看到 RASPBERRYPIpiPublic 文件夹具有读/写权限。

使用 Visual Studio Code 进行调试

在首次打开文件夹时,使用读/写网络地址 "\\RASPBERRYPI\piPublic\foldername",以便正确设置 workspaceRoot 变量。调试 http 请求时,最好将断点设置在回调函数内部,而不是在父方法的开头。您还需要意识到浏览器可能会因命中断点而超时。

如果您从 vscode 启动应用程序,它将在本地计算机上运行,而不是在 Pi 上运行。要远程调试,您需要使用集成的 node.js 调试器。设置断点的方法是在您想要中断的源代码中添加关键字 debugger;。然后您可以在 vscode 中检查变量。这是设置方法。

  1. 配置 PuTTY 以允许在端口 5858 上进行连接。在 PuTTY 中,选择类别/连接/SSH/隧道。勾选“本地端口接受来自其他主机的连接”。将源端口输入为 5858,将目标输入为 localhost:5858,然后单击添加

    View

  2. 在“会话”窗口中,输入 Pi 的 IP 地址。确保选择了连接类型 SSH。在保存的会话框中为会话命名并保存它。双击保存的会话名称将加载它并打开终端。

    View

  3. 使用网络地址 "\\RASPBERRYPI\piPublic\HomeAuto" 导航到 Pi 上的应用程序目录,然后输入 'node --debug index.js' 来启动应用程序。在 vscode 调试模式下,从左上角的下拉菜单中选择“**附加到端口**”配置,然后按旁边的箭头开始调试。如果下拉列表中没有可用选项,则 launch.json 配置文件尚未构建;按调试按钮并按照提示进行构建。

    View

在启动时运行应用程序

这只是以root身份访问文件 /etc/rc.local 的问题。

sudo nano /etc/rc.local

然后,在最后一行正上方,添加应用程序的 start 命令。因此,最后两行如下所示

sudo node /home/pi/Public/HomeAuto/index.js
exit 0 

结论

希望这对您配置和编程 Raspberry Pi 有所帮助。您可能想自己尝试一下,这很有趣,而且您可以学到很多东西。

历史

  • 2017 年 2 月 4 日:初始版本
© . All rights reserved.