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

YouCar-Intel Edison 多模态实时 YouTube 直播智能汽车

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2016年8月2日

CPOL

30分钟阅读

viewsIcon

30318

downloadIcon

638

实时流式传输多模态控制的 Intel Edison 智能遥控汽车。

目录

1. 动机

2. 系统设计

3. 开始破解

4. 为遥控车编程

    4.1 用于控制遥控车的 Node.js 物联网设备应用

    4.2 Android Studio 安卓原生应用

    4.3 用于通过手势、情绪和语音控制小车的 Intel RealSense C# 客户端

5. 搭建实际原型

6. 从 YouCar 到 YouTube 的直播

7. 结论

1. 动机

- 我们将破解一辆遥控车。

-那又怎样?这是最老套的破解了。

-我们将通过手机控制这辆车。

- 呵呵,我见过很多用蓝牙破解遥控车的。没什么新意。

-我们将使用手势来控制这辆车。

- 我也看过 Intel Real Sense 遥控车的破解演示。

- 我们将提供语音控制功能。

- 微软语音 SDK 比我的第一个钱包还老。

- 你也可以用普通的遥控器来控制这辆车。

- 哦,所以你打算就这些已有的破解方式各写 5 篇文章?

- 不。我们的车将是多模态的。所有功能集于一个项目。听起来有趣吗?

-嗯,有点意思。但那又怎样?制作不同的模块并将其添加到汽车控制中并没有什么创新。

-我们的车还会对人类的情绪做出反应。而且,我们的车可以自动行驶,避开障碍物。

- 从来没听说过!

- 但是等等……这有什么用?除了是一个很酷的破解项目,它到底解决了什么问题?

-我们的车将是一辆视频直播车。它会向 YouTube 直播现场活动。所以你可以把它当作一辆摄像车来使用。

 

自从地球上第一个 DIY 项目诞生以来,黑客们最先尝试的可能就是用某种控制方式来控制某种车辆。事实上,上次 Codeproject 举办物联网竞赛时,我就写过一个获得大奖的名为 UROBI 的多模态控制机器人框架

所以,如果我再写一个多模态项目,我可能自己都会觉得无聊而无法完成。即使我能设法激励自己完成写作,也几乎不会有读者(除了评委,因为他们是被迫阅读的!!!!)。

为了避免浪费宝贵的数字字节和我们程序员、黑客朋友们的宝贵时间,我想进行一个既有趣又能解决问题的、具有挑战性的物联网项目。

所以,我并不想写一个普通的 DIY 项目。物联网已经发展了。借助新功能、新设备和新 API,物联网可以解决现实世界的问题。到目前为止,在某种机器人车辆上安装摄像头并不难,但要真正用这个摄像头向 YouTube 直播活动,(据我所知)还从未有人尝试过。想象一下,你举办了一个家庭派对,你的 YouCar 在房间里自动移动,响应你的声音、手势和手机指令,并将派对实况直播到 YouTube。这就是我们这篇文章要构建的——YouCar。

听起来有趣吗?请继续阅读。

Codeproject 福利:和我大多数物联网本地服务一样,这个项目会有一个 C# 客户端为你处理所有事务。所以,即使你不是机器人或 DIY 爱好者,只要你是 C# 程序员,这个项目也适合你。

 

我们将使用 Intel Edison 作为我们的物联网设备,首先因为我是一名英特尔软件创新者,其次英特尔非常慷慨地免费为我提供了一台 Edison 设备来进行实验。无论你是否熟悉 Intel Edison,下面的教程(特别是 A 部分)都将帮助你很好地入门 Edison。

Intel Edison 入门 - 请参考 A 部分(文章即将发布)

2. 系统设计

从动机部分可以明显看出,我们将要构建(或破解)一辆能够响应多种不同设备和命令的遥控车。因此,在开始制作之前,理解其架构非常重要。

图 2.1:小车控制框图

图 2.2 破解概念

首先看图 2.2。我们可以看到一辆遥控车及其对应的遥控器。这些遥控车在各种标准的射频频率下运行。27MHz 是玩具遥控车非常流行的频率选择。这辆车有两个独立的电路:一个在遥控器里的发射器。遥控器上的每个开关都会在调谐频率上产生不同的脉冲。车内电路有接收电路,它接收这些脉冲,并相应地控制一个 H 桥,H 桥驱动负责小车运动的电机。在大多数商用玩具车中,有两个电机:一个用于左右移动,另一个用于前后移动。你可以同时控制这两个电机。因此,前进-右转、前进-左转、后退-右转、后退-左转成为四种可能的运动方式。

现在,如果你想把这样一辆车变成物联网车,你需要破解发射器、遥控器,或者直接破解汽车本身,并将 H 桥直接连接到你的物联网嵌入式设备电路中。

我大约一年前第一次破解遥控车,是为了让我儿子 Rupansh 能用手机开车。现在,你觉得孩子们会喜欢他们心爱的汽车里伸出乱七八糟的电路吗?绝不。所以最好的破解方法是破解发射器。

我们可以做的是,以某种方式将发射器(即遥控器)的四个开关与我们的物联网板连接起来,并通过编程方式复制开关的功能。在这里,我们将使用带有 Arduino 兼容扩展板的 Intel Edison 作为我们的物联网设备。图 2.2 很好地展示了我们的小车会是什么样子。

现在的问题是,我们如何复制这四个遥控按钮的功能?

图 2.1 有助于你更好地理解这个概念。我们将在物联网设备中运行一个 MqTT 服务器。不同的应用程序(手机或电脑)将向这个 MqTT 通道发布命令。根据这些命令,开发板将以编程方式控制这些开关。

这一切最棒的地方在于,你几乎可以为任何模态构建应用程序,只需将其连接到我们 Edison 设备订阅的 MqTT 通道,就大功告成了。

记住,我们还需要将摄像头连接到我们的车上,并直播到 YouTube。但是,首先我们需要破解这辆车,让它能够以编程方式工作。

 

准备好了吗?

3. 开始破解

图 3.1 打开遥控器

首先用一把小螺丝刀打开遥控器。你会看到遥控器内部的电路。一对电线(一红一黑)会从电路中出来,连接到电池盒。这两根线是你的电源线。你会看到前进和后退的控制杆以及左转和右转的按钮。我们不再需要那些控制杆了。所以只需拧下螺丝,把电路板取出来。

图 3.2 从遥控器外壳中分离电路

在图 3.2 中,你可以看到从遥控器外壳上拧下后分离出来的电路。现在你可以按下按钮,看到你的小车在移动。观察电路是如何由电池盒供电的。

我们的第一步是将这两根电线从电池盒上剪断,然后从我们的设备上给它供电。

图 3.3 将 Grove 电源连接到遥控器

为此,你需要一根 Grove 连接线。这种线缆两端各有一个端口,一个用于将线缆连接到 Grove 扩展板,另一个用于连接到像 LED 这样的 Grove 组件模块。你需要剪掉其中一个端口。这样,线缆的一个端口连接到扩展板,另一端则有四根线:红、黑、黄、白。如果你观察 Grove 扩展板顶部任何一个端口的标签,你会很容易明白红线对应 Vcc,黑线对应地线,黄线对应该端口的引脚。要给遥控模块供电,你需要将之前连接到电池盒的红线和黑线分离出来,并将它们与你刚刚剪掉端口的线缆的红线和黑线连接起来。图 3.3 会让你对这个连接有更清晰的了解。

一旦你用 Grove 扩展板为遥控模块供电后,请确保电源能够正确驱动遥控器。打开你的小车,现在手动按下按钮,看看小车是否会移动。

那么,你刚刚把你的遥控模块连接到了 Edison 开发板上。但实际上,它仍然是通过其板载的按钮来控制的。对吧?所以,我们的下一个任务是把这些按钮的控制权交给我们的 Edison 开发板。

在我们了解如何破解它们之前,建议你阅读我的 Arduino 初学者指南文章中的几个小节。

在我们当前的破解中,我们将用开关替换直流电机。

那篇文章(在“控制直流电机”部分)的图 9.1 应该会让你感兴趣。在那张图中,你可以看到三根线从 Arduino 引出:红色来自 VCC,黑色来自地线,绿色来自引脚 9。我们所要做的就是取出 BC 548 的引脚 1 和引脚 3,并将它们连接到遥控器上。

所以,如果我们为 Arduino 破解遥控器,它会像图 3.4 那样。

图 3.4:针对 Arduino 的遥控器破解

由于 Intel Edison 是 Arduino 兼容板,电路保持相似。BC 548/547 通过一个 100 欧姆的电阻与 Grove 连接线的黄线相连,红线与遥控器的红线(或 Vcc)相连,连接线的黑线与遥控器的黑线相连。就是这样。你需要四个晶体管和四个电阻来绕过所有的开关。

你可以使用面包板,但我更习惯在通用 PCB 板上焊接。成功破解遥控器后,最终的电路看起来类似于图 3.5。

图 3.5:完整遥控器破解的最终电路图

电路完成后,只需将连接线的端口部分连接到 Grove 扩展板上(D5-前进,D6-后退,D4-右转,D3-左转)。电路如下图所示。

图 3.6 连接到 Intel Edison 的最终遥控器破解

4. 为遥控车编程

我们仍在努力让我们的车与 Intel Edison 配合工作,也许第一步是与我们的手机配合。所以,这个编码部分将包括为 Edison 编写一个应用程序(物联网应用)和相应的移动应用程序(在物联网生态系统中称为配套应用)。

4.1 用于控制遥控车的 Node.js 物联网设备应用

在我们生物识别锁项目 A 部分的 MqTT 章节中,我们已经看到了如何使用一个全局的 iot.eclipse.org 消息代理来有效地作为你的客户端应用和物联网应用之间的桥梁。然而,遥控车必须更具响应性。MqTT 消息存在延迟,无论多么微小。这种延迟在用户体验方面是极其恼人的。你希望汽车能够快速响应,并且在瞬间响应命令。所以,我们不能依赖全局代理,而需要在 Intel Edison 上运行一个本地代理。幸运的是,这也非常容易。

Mosquitto 是一个运行在 Linux 上的物联网 MqTT 消息代理。所以我们先来安装它。

在 Edison 上安装 Mosquitto MqTT 代理

  • 在 Edison Shell 中,输入并回车以下命令,将 Mosquitto 压缩包下载到你的 /home/root 目录中。
wget http://mosquitto.org/files/source/mosquitto-1.3.5.tar.gz
  • 解压 Zip 文件夹
tar xzf mosquitto-1.3.5.tar.gz
  • Install
cd mosquitto-1.3.5

make WITH_SRV=no 

就这样。你的 Intel Edison 将在启动时于端口 1883 运行一个 Mosquitto MqTT 代理。

  • 测试

打开两个 PuTTY 终端,通过 SSH 连接到你的开发板。在一个 shell 中,订阅消息。比方说,我们在这里使用一个名为 rupam/data 的通道。这里的 CHANNEL_NAME 将是 rupam/data。

mosquitto_sub -h EDISON_IP -p 1883 -t CHANNEL_NAME

在另一个终端发布消息。

mosquitto_pub -h EDISON_IP -p 1883-t CHANNEL_NAME -m "SOME_MESSAGE"

就是这样,你会在你订阅的第一个 shell 中看到发布出来的命令或消息。

  • 用手机测试

如果你是 Android 用户并立志成为物联网开发者,那么拥有 MyMQTT 应用总是一个好主意。在 MyMqtt 中,为代理 EDISON_IP 订阅 CHANNEL_NAME。

如果你不确定 EDISON_IP,ifconfig 命令会产生很多 IP 地址。请在 wlan0 部分查找你的设备 IP 地址。

Node.js 代码

这段代码的核心是在 mqtt 模块中订阅 rupam/data(或者任何你喜欢的通道)。然后根据接收到的命令控制引脚 D3、D4、D5 和 D6。

需要注意的一个重要部分是,RIGHT(右转)和 LEFT(左转)命令只有在与 FORWARD(前进)和 REVERSE(后退)结合使用时才有意义。此外,在 FORWARD 或 REVERSE 模式下,汽车可能有 RIGHT、LEFT 或 Straight(直行,即无 RIGHT 或 LEFT)的移动。

因此,我们相应地开发了两个额外的命令:NO 和 STOP。NO - 当 LEFT=0 且 RIGHT=0 时;STOP - 当所有开关都为 0 时。

 

var mqtt = require('mqtt');

var client = mqtt.connect('mqtt://192.168.1.101');// chenge this to edison_local_ip. use ifconfig and
//look for wlan0 to knwo the ip address

// mqtt:// is important. Else code will not work. So if you are testing with mosquitto.org

// mqtt://test.mosquitto.org

var mraa = require('mraa')

var SPEED=1

////////////////// Remote Pins//////////////////

var fPin = new mraa.Gpio(6)

var bPin = new mraa.Gpio(5)

var rPin = new mraa.Gpio(3)

var lPin = new mraa.Gpio(4)

//////////////Set up Pins/////////////////////////

fPin.dir(mraa.DIR_OUT)

bPin.dir(mraa.DIR_OUT)

rPin.dir(mraa.DIR_OUT)

lPin.dir(mraa.DIR_OUT)

//////////////// Initialize///////////////////

fPin.write(0)

bPin.write(0)

rPin.write(0)

lPin.write(0)

/////////////////////////////////

client.subscribe('rupam/data/#')// change it as per your wish.

// # at the end is important. Else your code is not going to work

client.handleMessage=function(packet,cb) {

var payload = packet.payload.toString()

if (payload === 'FORWARD')

{

bPin.write(0);

fPin.write(SPEED)

}

if (payload === 'REVERSE')

{

fPin.write(0);

bPin.write(SPEED);

}

if (payload === 'RIGHT')

{

lPin.write(0);

rPin.write(1)

}

if (payload === 'LEFT')

{

rPin.write(0); lPin.write(1)

}

if (payload === 'STOP')

{

fPin.write(0)

rPin.write(0)

bPin.write(0)

lPin.write(0)

}

if (payload === 'NO')

{ rPin.write(0)

lPin.write(0)

}

console.log(payload) cb()

}

////////////////////////////////////////////// Code Ends////////////////////////////////////////////

fPin、bPin、rPin 和 lPin 是连接 FORWARD(前进)、BACK(后退)、RIGHT(右转)和 LEFT(左转)开关的引脚。请注意,我们声明了一个名为 speed 的变量但尚未使用它。我们稍后会用它通过 PWM 驱动晶体管来控制前进和后退电机的速度。这就是为什么前进和后退按钮被连接到支持 PWM 的引脚上。但目前,我们只想让这辆车动起来。虽然你可以用 MyMqtt 应用测试移动,但这不是一个好主意,因为在你输完 STOP 之前,车可能已经撞到墙了。

请注意,当处于前进模式时,后退电机会关闭,反之亦然。当处于右转模式时,左转电机会关闭,反之亦然。当收到 STOP 命令时,所有电机都会关闭。当收到 NO 命令时,左转和右转电机会关闭。

现在我们将构建一个 Android Studio 原生安卓应用来控制这辆车。

4.2 Android Studio 安卓原生应用

这个应用的想法非常简单。提供一种连接到我们的 Mqtt 代理并向我们的通道发布消息的方式。我们将使用 Eclipse Paho Mqtt 库进行通信。

我们先在 activity_main.xml 布局文件中创建我们的图形用户界面(GUI)。

请看图 4.1 中的布局设计。

图 4.1:用于控制小车的简单 UI 设计

我们使用相对布局来放置组件。这是 xml 文件,

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="Broker" android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textView" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="FORWARD"
        android:id="@+id/button"
        android:layout_below="@+id/edChannel"
        android:layout_centerHorizontal="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Small Text"
        android:id="@+id/textView2"
        android:layout_alignParentBottom="true"
        android:layout_alignRight="@+id/button"
        android:layout_alignEnd="@+id/button" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="REVERSE"
        android:id="@+id/button2"
        android:layout_above="@+id/textView2"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="119dp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="LEFT"
        android:id="@+id/button3"
        android:layout_below="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginTop="55dp"
        android:layout_toStartOf="@+id/editText"
        android:layout_toLeftOf="@+id/editText" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="RIGHT"
        android:id="@+id/button4"
        android:layout_alignTop="@+id/button3"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/editText"
        android:hint="192.168.1.102"
        android:layout_alignParentTop="true"
        android:layout_alignRight="@+id/button2"
        android:layout_alignEnd="@+id/button2"
        android:text="192.168.1.101" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Connect"
        android:id="@+id/button5"
        android:layout_alignTop="@+id/editText"
        android:layout_toRightOf="@+id/button"
        android:layout_toEndOf="@+id/button" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Channel"
        android:id="@+id/textView3"
        android:layout_below="@+id/editText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/edChannel"
        android:layout_below="@+id/editText"
        android:layout_alignLeft="@+id/editText"
        android:layout_alignStart="@+id/editText"
        android:layout_alignRight="@+id/editText"
        android:layout_alignEnd="@+id/editText"
        android:text="rupam/data" />

</RelativeLayout>

下载适用于安卓的 mqtt jar 文件,进入项目的物理目录,并将 jar 文件粘贴到 App/libs 文件夹中。如果该文件夹不存在,请创建它。

你现在应该可以在 Android Studio 的项目选项卡下看到这个 jar 文件了(见图 4.2)。

 

图 4.2 将 Mqtt jar 文件添加到项目中

回想一下,在我们的 Mqtt Node.js 应用中有六种状态:FORWARD(前进)、REVERSE(后退)、LEFT(左转)、RIGHT(右转)、STOP(停止)和 NO(无转向)。所以,要控制这个应用,你实际上需要六个按钮。但遥控器上没有六个按钮,对吧?当你按下遥控器按钮时,车会移动;当你松开时,车会停止。因此,我们可以将 MqTT 发布附加到触摸事件上,而不是按钮点击事件。当右转和左转按钮被释放时发送 NO,当前进/后退按钮被释放时发送 STOP。主题会发布到 edChannel 中指定的通道。

在 MainActivity.java 类中,我们创建了一个 Connect() 方法用于连接到代理。由于 Android 不允许从主线程访问网络调用,我们创建了一个名为 ConnectionClass 的新类,它继承自 AsyncTask,并在 doInBackground 中连接到代理。

 

public class ConnectionClass extends  AsyncTask<String, String, String>
{
    Exception exc = null;

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }
    @Override protected String doInBackground(String... params) {
        try
        {
            if (Looper.myLooper()==null)
                Looper.prepare();
            sampleClient = new MqttClient(broker, clientId, persistence);

            //    sampleClient=new MqttClient(broker,clientId);
            connOpts = new MqttConnectOptions();
            connOpts.setCleanSession(true);
            IMqttToken imt=sampleClient.connectWithResult(connOpts);

            Log.d("MQTT MODULE.....","....DONE..."+sampleClient.getServerURI()+"--"+imt.getResponse().getPayload());


            if(sampleClient.isConnected()) {
                return "CONNECTED";
            }
            else
            {
                return "Connection Failed.....";
            }

        }
        catch(Exception ex )
        {

            Log.d("MQTT MODULE", "CONNECTion FAILED " + ex.getMessage() + " broker: " + broker + " clientId " + clientId);

            return "FAILED "+ex.getMessage();
        }
        // return null;
    }

    @Override protected void onPostExecute(String result) {
        super.onPostExecute(result);

        if(result!= null)
        {
          //////////// Added later ( so that connection can be checked before publish/////////////
           if(result.Equals("CONNECTED"))
           {
             isConnected=true;
           }
           else
           {
             isConnected=false;
           }
          ////////////////////////////
            tv2.setText(result);
            Toast.makeText(MainActivity.this, result, Toast.LENGTH_LONG).show();
            // setContentView(result);
        }
    }
}

postExecute 方法中,我们用一个 Toast 显示连接请求的结果。

当点击 btnConnect 时,会用 ConnectionClass 的对象调用 execute() 方法,从而在后台触发连接。

btnConnect.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        broker="tcp://"+edServer.getText().toString().trim()+":1883";
        topic=edChannel.getText().toString().trim();
        ConnectionClass con=new ConnectionClass();
        con.execute();
    }
});

我本可以为六种状态创建六个按钮:前进、后退、左转、右转和无转向,并从按钮的 onClickListener 生成相应的 MqTT 命令。但这不太直观。回想一下物理遥控器是如何工作的。当你按住一个按钮时,汽车处于该状态,当你松开按钮时,汽车停止。我想模仿这个原理。所以,我们不像物理遥控器那样创建六个按钮,而是创建四个按钮,并且不将事件附加到 onClickListener,而是将 setOnTouchListener 附加到按钮上。当触摸被按下时,我们生成与按钮文本相对应的 MqTT 消息;当触摸被释放时,如果释放的按钮是左转或右转,我们生成 NO;如果释放的按钮是前进或后退,我们生成 STOP。

这是在 mainActivity.java 中所有 UI 组件的初始化代码:

b = (Button)findViewById(R.id.button);
  b2=(Button)findViewById(R.id.button2);
  b3=(Button)findViewById(R.id.button3);
  b4=(Button)findViewById(R.id.button4);
btnConnect=(Button)findViewById(R.id.button5);
  edServer=(EditText)findViewById(R.id.editText);;
  edChannel=(EditText)findViewById(R.id.edChannel);
  topic=edChannel.getText().toString().trim();

  btnConnect.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
          broker="tcp://"+edServer.getText().toString().trim()+":1883";
          topic=edChannel.getText().toString().trim();
          ConnectionClass con=new ConnectionClass();
          con.execute();
      }
  });
//  b.setOnClickListener(this);
  b.setOnTouchListener(this);
  b2.setOnTouchListener(this);        //b.setOnLongClickListener(this);
  b3.setOnTouchListener(this);        //b.setOnLongClickListener(this);
  b4.setOnTouchListener(this);        //b.setOnLongClickListener(this);
  tv2=(TextView)findViewById(R.id.textView2);

现在下一步是处理触摸事件。让 MainActivity.java 类实现 OnTouchListener 接口。

在 onTouch 方法中,我们处理 MotionEvent.ACTION_DOWNMotionEvent.ACTION_CANCEL 来触发 MqTT 消息。

@Override
public boolean onTouch(View v, MotionEvent event) {

    Button b=(Button)v;
    switch (event.getAction() & MotionEvent.ACTION_MASK) {

        case MotionEvent.ACTION_DOWN:
            v.setPressed(true);
            // Start action ...
            tv2.setText("ENTERED:"+b.getText()+"-" + (new Date()).toString());
            Send(b.getText().toString());
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_OUTSIDE:
        case MotionEvent.ACTION_CANCEL:
            v.setPressed(false);
            // Stop action ...
            tv2.setText("LEFT:" + b.getText() + "-" + (new Date()).toString());
           if(b.getText().toString().trim().equals("LEFT")||b.getText().toString().trim().equals("RIGHT"))
               Send("NO");
            else
            Send("STOP");
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            break;
        case MotionEvent.ACTION_POINTER_UP:
            break;
        case MotionEvent.ACTION_MOVE:
            break;
    }

    return true;
}

正如你所见,MqTT 消息实际上是通过 Send() 方法生成的。

Send() 方法中,我们检查 sampleClient 是否已连接,如果已连接,我们就将消息发送到由 edChannel EditText 指定的通道中。

void Send(String content)
{
    final String data=content;
  // isConnected =sampleClient.isConnected();
    AsyncTask.execute(new Runnable()
    {
        @Override
        public void run()
        {

            try
            {
                if(isConnected)// Added to check the connection before publishing
                {
                    MqttMessage message = new MqttMessage(data.getBytes());
                    message.setQos(qos);
                    sampleClient.publish(topic, message);
                }

                Log.d("MQTT MODULE",data+" SENT");
            }
            catch(Exception ex)
            {

            }
            //TODO your background code
        }


    });
}

 

由于 Android 需要明确的用户权限才能访问网络,请不要忘记添加

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

<application> 标签之前。

图 4.3:运行 Android 应用

就是这样。你现在已经准备好用你的遥控车大显身手了。

请记得在你的 Node.js 应用中更改代理地址。

图 4.4:我儿子 Rupansh 用手机控制小车(点击观看演示视频)

4.3 用于通过手势、情绪和语音控制小车的 Intel RealSense C# 客户端

英特尔实感(RealSense)是用于短距离精确手势识别的伟大技术之一。它在推出时曾引起巨大轰动。早期,游戏被认为是该技术的最佳应用场景之一。随着时间的推移,增强现实和 3D 扫描已经超越了其他应用场景。然而,英特尔正支持该技术成为物联网的一部分,作为其最佳应用案例。事实上,英特尔的机器人开发套件就支持实感技术。当你观察机器人技术的最新趋势时,你会发现英特尔实感正缓慢而稳定地成为一些最新大众市场机器人的一部分。随着华硕 Zenbo 的出现,手势技术正在被重新定义。它实际上是第一款大众市场的机器人。

如果你观察视频,你会意识到像“zenbo,把那条毛巾拿给我”这样周期性的手势,并用手指着毛巾,可以引导机器人更好地定位物理物体。

因为我相信,将语音和手势结合起来,是控制传统上通过某种遥控器和开关来控制的物理物体的最佳输入方式之一。所以我想,为什么不为我们的小车尝试一个 RealSense 应用呢?

我们设计最棒的一点是,可以在不改变任何硬件的情况下将应用程序添加到架构中。例如,我们可以开发一个新的 Android 应用程序,用加速度计来控制汽车的左右方向。我们所要做的就是将检测到的手势推送到我们的 MqTT 通道中。

因此,RealSense 应用是一个独立的单元,可以与我们的智能手机 Android 应用一起使用,也可以完全作为独立应用使用。然而,最大的挑战是创建一个能够同时处理语音、面部和手势的多模态应用。

已经使用过 RealSense 的朋友们会同意,这项技术的多重手势响应相当困难,而且非常麻烦。SDK 附带的大多数示例都是独立的应用案例,即手部跟踪、3D 分割、语音合成以及用 RealSense 控制桌面 UI(鼠标模式)都有各自不同的示例。

所以我创建了一个名为 TheUltimateRealSense 的类,在其中我小心地结合了面部、分割、手部追踪和语音识别(以及语音合成!)。

让我们先看看各种手势,然后再看它们在我们的控制机制中是如何工作的。

图 4.5 RealSense 模块中的手部和情绪检测

这里是工作流程和手势解读:

  • 用户可以为“前进”、“后退”、“左转”、“右转”、“停止”和“无转向”生成语音命令。
  • 当用户发出前进的语音命令时,汽车的前进运动将保持激活状态。
  • 用户可以通过语音手势、显示手部闭合手势或产生惊讶情绪来停止汽车(我将向你解释选择这种情绪来停车的逻辑!!)。
  • 用户可以将手靠近或远离摄像头,分别对应前进和后退命令。
  • 手可以向左或向右移动,以实现左转和右转。如果手在向右移动时离摄像头较近,则为“前进+右转”。同样,如果手在远离摄像头的位置向右移动,则为“后退+右转”。
  • 当汽车的运动是由手势激活的,一旦手放下(未检测到手),就会生成 STOP 命令。然而,如果汽车是由语音命令启动的,那么手放下不会停止汽车。这意味着,你可以发出语音命令后坐下来放松。

我们在 MainForm.cs 中创建了一个名为 DoRendering 的新线程。

private void Start_Click(object sender, EventArgs e)
        {
            Start.Enabled = false;
            Stop.Enabled = true;
            stop = false;
            System.Threading.Thread thread = new System.Threading.Thread(DoRendering);
            thread.Start();
            System.Threading.Thread.Sleep(5);
        }

创建了 TheUltimateRealSense 类的一个对象,并将 MainForm 的实例传递给它的 MainForm 对象 form。这样,该类就可以访问 MainForm 的元素。

这个类有一个名为 pp 的流水线,用于访问 RealSense 算法。

PXCMSenseManager pp;

调用 pp.QueryHand()pp.Query3DSeg()pp.QuerySample()pp.QueryEmotion()pp.QueryHandSample() 来检查已安装的 SDK 组件是否支持这些功能,以及连接的摄像头是否可用于这些实现。

StreamColorDepth() 是进行所有检测的主要方法。

 

 private void DoRendering()
        {
           rs= new TheUltimateRealSenseClass(this,session,StreamMode.LIVE,ColorType.SEGMENTED);
           rs.timer = new FPSTimer(this);
           rs.OneTimeInitializationBeforeLooping();
           rs.Speak("Welcome to Smart I o T RC Car Demo", 80, 100, 80);
           int choice = 0;//0->segmented 1->hand
           while (!rs.Stop)
           {

               bool success = rs.StreamColorDepth(true);
               if (success)
               {
                   lock (this)
                   {
                       if (choice == 0)
                       {
                         //  bitmaps[index] = rs.bitmaps[index]; // for segmented image
                          bitmaps[index] = rs.DisplayJoints(rs.nodes, rs.numOfHands, rs.bitmaps[index], true, true, true);
                       }
                       if (choice == 1)
                       {
                           if (rs.labeledBitmap != null)
                           {
                               //bitmaps[index] = rs.DisplayJoints(rs.nodes, rs.numOfHands, rs.labeledBitmap, true, true, true);
                               bitmaps[index] = rs.labeledBitmap;
                           }
                       }
                       if(rs.emotionData!=null)
                       bitmaps[index] = rs.DisplayEmotionSentimentFaceLocation(rs.emotionData,bitmaps[index]);
                   }
                   MainPanel.Invalidate();
                   UpdatePanel();
               }

              
           }
           this.Invoke(new DoRenderingEnd(
                 delegate
                 {
                     rs.Finish();
                     Start.Enabled = true;
                     Stop.Enabled = false;
                     MainMenu.Enabled = true;
                     if (closing) Close();
                 }
             ));
        }

在 StreamColorDeapth() 中,我们首先分割图像(并不是说这个极度消耗资源的方法有什么影响,我加入它是为了将来可以在需要背景分割的应用中使用)。

if (pp.AcquireFrame(synced) < pxcmStatus.PXCM_STATUS_NO_ERROR)
            {
                projection.Dispose();
                return false;
            }
            frameCounter++;
            /* Get Segmentation image from the User Segmentation video module */
            PXCM3DSeg seg = pp.Query3DSeg();
            if (seg == null)
            {
                pp.ReleaseFrame();
                projection.Dispose();
                pp.Close();
                pp.Dispose();
                UpdateStatus("Error: 3DSeg is not available");
                return false;
            }
            PXCMImage segmented_image = seg.AcquireSegmentedImage();
            if (segmented_image == null)
            {
                pp.ReleaseFrame();
                projection.Dispose();
                pp.Close();
                pp.Dispose();
                UpdateStatus("Error: 3DSeg did not return an image");
                return false;
            }

在 StreamColorDeapth() 中,一旦我们分割了图像,我们就会查询 HandSample,它会返回一个分割后的手部图像(如果检测到手的话)。如果存在分割后的手部图像(顺便说一下,在我们的案例中并未使用),那么我们就会遍历检测到的手的数量,并调用 QueryhandData(),它会在 ihandData 中返回手部统计数据。这会进一步被查询以获取质心(手的位置)和张开度。这些统计数据被传递给 HandGestureToCar(),用于将手部位置转换成特定于汽车控制的手势。

#region hand gesture related part
            if (handData != null)
            {
                handData.Update();
            }

            sample = pp.QueryHandSample();
            if (sample != null && sample.depth != null)
            {
                DisplayPicture(sample.depth, handData); //working
                //DisplayPicture(segmented_image, handData);
                if (handData != null)
                {
                    frameNumber = liveCamera ? frameCounter : pp.captureManager.QueryFrameIndex();

                    //DisplayJoints(handData);
                    #region display joints
                    PXCMHandData handOutput = handData; long timeStamp = 0;
                    //Iterate hands
                    nodes = new PXCMHandData.JointData[][] { new PXCMHandData.JointData[0x20], new PXCMHandData.JointData[0x20] };
                    numOfHands = handOutput.QueryNumberOfHands();
                    if (numOfHands == 0)
                    {
                        form.HandGestureTocar(posX, posY, posZ, 0);
                    }
                    for (int i = 0; i < numOfHands; i++)
                    {
                        //Get hand by time of appearence
                        //PXCMHandAnalysis.HandData handData = new PXCMHandAnalysis.HandData();

                        if (handOutput.QueryHandData(PXCMHandData.AccessOrderType.ACCESS_ORDER_BY_TIME, i, out ihandData) == pxcmStatus.PXCM_STATUS_NO_ERROR)
                        {
                            if (ihandData != null)
                            {
                                HandOpen[i] = ihandData.QueryOpenness();
                                var pt = ihandData.QueryMassCenterImage();
                                posX = pt.x;
                                posY = pt.y;
                                posZ = ihandData.QueryMassCenterWorld().y*1000;
                                form.Invoke((MethodInvoker)delegate
                                {
                                    form.Text = String.Format("X={0} Y={1} Z={2} OpenNess={3}", posX, posY, posZ,HandOpen[i]);
                                    form.HandGestureTocar(posX,posY,posZ,HandOpen[i]);
                                    // your UI update code here. e.g. this.Close();Label1.Text="something";
                                });
                               
                                //Iterate Joints
                                for (int j = 0; j < 0x20; j++)
                                {

                                   //ihandData.QueryTrackedJoint((PXCMHandData.JointType)j, out jointData);
                                   //nodes[i][j] = jointData;

                                } // end iterating over joints
                            }
                        }
                    } // end itrating over hands

                    #endregion

 public void HandGestureTocar(double x,double y,double z,int openness)
        {
            
            if (openness > 30)
            {
                
                //if (Math.Abs(lastY - y) > 60)
                {
                    
                      if (x < 250 )
                        {
                            gesture = "LEFT";
                        }
                      else if (x > 420)
                      {
                          gesture = "RIGHT";
                      }
                      else
                      {
                          if (Math.Abs(z) < 28)
                          {
                              if (lastGesture.Equals("LEFT") || lastGesture.Equals("RIGHT"))
                              {
                                  gesture = "NO";
                              }
                              else
                              {
                                  gesture = "FORWARD";
                              }
                          }
                          if (Math.Abs(z) > 40)
                          {
                              if (lastGesture.Equals("LEFT") || lastGesture.Equals("RIGHT"))
                              {
                                  gesture = "NO";
                              }
                              else
                              {
                                  gesture = "REVERSE";
                              }
                          }
                      }
                        
                        
                   
                    lastY = y;
                }
            }
            else
            {
                lastY = 0;
                if(!TheUltimateRealSenseClass.isVoiceGesture)
                gesture = "STOP";
            }

            if (!gesture.Equals(lastGesture))
            {
                label1.Invoke((MethodInvoker)delegate
                {
                    label1.Text = gesture;
                });
                               
                
                if (connected )
                {
                    lastGesture = gesture;

                    lastY = 0;
                    PublishMQTTData(gesture);
                    
                }
            }
        }

根据 Z 轴,确定手与摄像头的距离,这用于控制前进和后退。如果手的张开度小于 30,则假定为手闭合手势,生成 STOP 命令。如果手处于前进状态,检查 X 轴以确定左转和右转。我们将先前检测到的手势存储在 lastGesture 中。如果手之前在右侧,现在移到了中间,汽车的右转电机必须用 NO 命令停止。所以,我们使用 lastGesture 来了解上一个检测到的手势是什么。如果上一个是左转或右转,而当前未检测到左转/右转,我们就生成 NO 命令。

如果前进或后退命令是由语音模块生成的,那么没有手部出现不会触发任何命令。否则,会生成 STOP 命令。

当用户通过语音发出前进、后退、左转或右转命令时,isVoiceGesture 变为 true;当任何模块生成 STOP 命令时,它会重置为 false。

惊讶的面部表情将导致 STOP(停止)命令。

现在,你脑中可能会出现一个问题:“我们为什么需要表情来控制设备,或者就这件事来说,控制汽车?”想象一下,你正在开派对并进行直播,而你的汽车正要撞上你妻子在周年纪念日送给你的那个珍贵小礼物,你会怎么做?有趣的是,我们常常对这种情况做出反应,却忘了该怎么做。不幸的是,英特尔实感技术在你汽车即将撞坏你妻子礼物的那一刻,检测到的是“惊讶”而不是“恐惧”。所以,为了方便各位男士,我加入了这个功能。这也展示了机器如何对自然表情做出反应。

所以,我们从 TheUltimateRealSenseClas 类的 DisplayEmotionSentimentFaceLocation() 方法中,分析情绪,并在检测到情绪为“惊讶”时生成 STOP 命令。

在这个方法中,我们首先调用 QueryNumFaces(),它返回场景中存在的人脸数量,然后对每个人脸调用 QueryAllEmotionData(),并使用 DrawLocation() 方法进行渲染。

 

public Bitmap DisplayEmotionSentimentFaceLocation(PXCMEmotion ft, Bitmap bitmap)
        {
            int numFaces = ft.QueryNumFaces();
            /*
            if (numFaces == 0)
            {
                form.PublishMQTTData("STOP");
            }*/
            for (int i = 0; i < numFaces; i++)
            {
                /* Retrieve emotionDet location data */
                PXCMEmotion.EmotionData[] arrData = new PXCMEmotion.EmotionData[NUM_EMOTIONS];
                if (ft.QueryAllEmotionData(i, out arrData) >= pxcmStatus.PXCM_STATUS_NO_ERROR)
                {
                    bitmap=DrawLocation(arrData,bitmap);
                }
            }
            return bitmap;
        }

检测到的“惊讶”情绪会产生一条“停止”的 MqTT 消息。

 

private Bitmap DrawLocation(PXCMEmotion.EmotionData[] data, Bitmap bitmap)
        {
            lock (this)
            {
                if (bitmap == null) return null;
                Graphics g = Graphics.FromImage(bitmap);
                Pen red = new Pen(Color.Red, 3.0f);
                Brush brush = new SolidBrush(Color.Red);
                Font font = new Font("Tahoma", 11, FontStyle.Bold);
                Brush brushTxt = new SolidBrush(Color.Cyan);
                
                    Point[] points4 = new Point[]{
                        new Point((int)data[0].rectangle.x,(int)data[0].rectangle.y),
                        new Point((int)data[0].rectangle.x+(int)data[0].rectangle.w,(int)data[0].rectangle.y),
                        new Point((int)data[0].rectangle.x+(int)data[0].rectangle.w,(int)data[0].rectangle.y+(int)data[0].rectangle.h),
                        new Point((int)data[0].rectangle.x,(int)data[0].rectangle.y+(int)data[0].rectangle.h),
                        new Point((int)data[0].rectangle.x,(int)data[0].rectangle.y)};
                    try
                    {
                        g.DrawLines(red, points4);
                    }
                    catch
                    {
                        brushTxt.Dispose();
                    }
                    //g.DrawString(data[0].fid.ToString(), font, brushTxt, (float)(data[0].rectangle.x + data[0].rectangle.w), (float)(data[0].rectangle.y));
               

                bool emotionPresent = false;
                int epidx = -1; int maxscoreE = -3; float maxscoreI = 0;
                for (int i = 0; i < NUM_PRIMARY_EMOTIONS; i++)
                {
                    if (data[i].evidence < maxscoreE) continue;
                    if (data[i].intensity < maxscoreI) continue;
                    maxscoreE = data[i].evidence;
                    maxscoreI = data[i].intensity;
                    epidx = i;
                }
                if ((epidx != -1) && (maxscoreI > 0.4))
                {
                    try
                    {
                        //form.PublishMQTTData
                        if (EmotionLabels[epidx].Equals("SURPRISE"))
                        {
                            form.PublishMQTTData("STOP");
                        }
                        g.DrawString(EmotionLabels[epidx], font, brushTxt, (float)(data[0].rectangle.x + data[0].rectangle.w), data[0].rectangle.y > 0 ? (float)data[0].rectangle.y : (float)data[0].rectangle.h - 2 * font.GetHeight());
                    }
                    catch
                    {
                        brush.Dispose();
                    }
                    emotionPresent = true;
                }

                int spidx = -1;
                if (emotionPresent)
                {
                    maxscoreE = -3; maxscoreI = 0;
                    for (int i = 0; i < (NUM_EMOTIONS - NUM_PRIMARY_EMOTIONS); i++)
                    {
                        if (data[NUM_PRIMARY_EMOTIONS + i].evidence < maxscoreE) continue;
                        if (data[NUM_PRIMARY_EMOTIONS + i].intensity < maxscoreI) continue;
                        maxscoreE = data[NUM_PRIMARY_EMOTIONS + i].evidence;
                        maxscoreI = data[NUM_PRIMARY_EMOTIONS + i].intensity;
                        spidx = i;
                    }
                    if ((spidx != -1))
                    {
                        try
                        {
                            g.DrawString(SentimentLabels[spidx], font, brushTxt, (float)(data[0].rectangle.x + data[0].rectangle.w), data[0].rectangle.y > 0 ? (float)data[0].rectangle.y + font.GetHeight() : (float)data[0].rectangle.h - font.GetHeight());
                        }
                        catch
                        {
                            red.Dispose();
                        }
                    }
                }

                brush.Dispose();
                brushTxt.Dispose();
                try
                {
                    red.Dispose();
                }
                finally
                {
                    font.Dispose();
                }
                g.Dispose();
            }
            return bitmap;
        }

你可以尝试修改代码,探索各种其他可能性,比如用微笑手势驾驶汽车,用面部移动来控制汽车等等。

我们在这里将要介绍的最后一种交互方式是语音。关于 RealSense 语音识别引擎,你必须知道的一个事实是,即使在有限的词汇或词典中,它在检测单个短语命令方面表现很差,但在检测大型词典中的复合短语方面却非常高效。所以,如果你的语音识别库包含了“前进”和“后退”作为语音命令,系统将无法有效检测到它们。

这也是因为会话在不同的 RealSense 算法之间是共享的。所以如果你的短语很短,很可能在你开始说话时,控制权还在手部或面部模块。一个复合且足够长的短语可以确保语音模块能捕捉到它。

我们声明一个名为 cmd 的字符串数组,并用命令对其进行初始化。

public string[] cmds = new string[] { "STRAIGHT FORWARD", "STOP STOP", "NO NO", "MOVE RIGHT", "MOVE LEFT", "COME REVERSE" };

语音模块或语音识别在 TheUltimateRealSense 类初始化时通过调用 OneTimeInitVoic() 方法进行初始化。在该方法中,我们设置默认音频设备,设置录音音量,创建一个 PXCMSpeechRecognition 的实例并将其附加到当前会话。我们使用 cmd 构建一个用于识别的语法。我们将 OnRecognition() 设置为事件处理程序,当短语被识别时会调用它。

public void OneTimeInitVoice(PXCMSession session)
        {

            /* Create the AudioSource instance */
            source = session.CreateAudioSource();

            if (source == null)
            {
                VoiceCleanUp();
                PrintStatus("Stopped");
                return;
            }

            /* Set audio volume to 0.2 */
            source.SetVolume(0.2f);

            /* Set Audio Source */
            source.SetDevice(vDevices[selectedVdevice]);

            /* Set Module */
            PXCMSession.ImplDesc mdesc = new PXCMSession.ImplDesc();
            mdesc.iuid = vModules[selectedVmodule];

            pxcmStatus sts = session.CreateImpl<PXCMSpeechRecognition>(out sr);
            if (sts >= pxcmStatus.PXCM_STATUS_NO_ERROR)
            {
                /* Configure */
                PXCMSpeechRecognition.ProfileInfo pinfo;
                sr.QueryProfile(0, out pinfo);
                sr.SetProfile(pinfo);

                if (cmds != null && cmds.GetLength(0) != 0)
                {
                    // voice commands available, use them
                    sr.BuildGrammarFromStringList(1, cmds, null);
                    sr.SetGrammar(1);
                }

                /* Initialization */
                PrintStatus("Init Started");
                PXCMSpeechRecognition.Handler handler = new PXCMSpeechRecognition.Handler();
                handler.onRecognition = OnRecognition;
                handler.onAlert = OnAlert;

                sts = sr.StartRec(source, handler);
                if (sts >= pxcmStatus.PXCM_STATUS_NO_ERROR)
                {
                    PrintStatus("Init OK");

                    /* Wait until the stop button is clicked */
                    

                   
                }
                else
                {
                    PrintStatus("Failed to initialize");
                }
            }
            else
            {
                PrintStatus("Init Failed");
            }

         
        }

所以当你从 cmd 中说出一个命令时,OnRecognition 会被调用,我们从这里发布我们的 MqTT 消息。

void OnRecognition(PXCMSpeechRecognition.RecognitionData data)
        {
          //  MessageBox.Show(data.scores[0].sentence);
            string s = data.scores[0].sentence;
            try
            {
                s = s.Split(new char[] { ' ' })[1];
                isVoiceGesture = true;
                VoiceGesture = s;

                form.PublishMQTTData(s);
                if(s.Equals("STOP")|| s.Equals("NO"))
                {
                    isVoiceGesture = false;
                }
            }
            catch { }
        }

当您生成语音手势时,isVoiceGesture 标志被设置为 true;当通过各自的短语生成 STOP 或 NO 命令时,该标志被设置为 false,从而允许用户使用其他模态。

现在,让我们看看消息发布是如何工作的。请记住,手势可能会在每一帧中被检测到。即使帧率大约是 10 帧/秒,你也总是在每 100 毫秒生成一个命令。就远程调用而言,这不是一个好的设计。再回想一下物理遥控器是如何工作的:按下会使汽车保持在特定状态,松开会重置状态。所以你所需要做的就是检查检测到的手势(或生成的命令)是否与前一个不同,只有在有新命令的情况下,才去打扰代理。

 string lastSent = "";
        public  void PublishMQTTData(string command)
        {
            label1.Invoke((MethodInvoker)delegate
            {
                label1.Text = command;
            });
                           
            if (connected)
            {
                if (!lastSent.Equals(command))
                {
                    if (command.Equals("STOP") || command.Equals("NO"))
                    {
                        TheUltimateRealSenseClass.isVoiceGesture = false;
                    }
                    mqtt.Publish(topic, GetBytes(command), 0, false);
                    lastSent = command;
                }
            }
        }

就是这样了。你可以根据自己的工作流程/命令和想象力,修改代码,用不同的方式控制这辆车。

5. 搭建实际原型

到目前为止,我们已经破解了一辆遥控车的遥控器,将其与 Intel Edison 连接,并玩了遥控车。但请记住,我们的项目不仅仅是一个破解。我们还希望为汽车实现自动导航,并希望将整个 Edison 板和破解装置都放在汽车本身上,这样我们就可以给它装上一个摄像头。这种设置的问题在于,带摄像头的 Edison 需要 12V 1.5A 的电源。所以你需要用外部电池为 Edison 供电。整个装置变得笨重。遥控车通常由 3V-6.5V @ 300mA-600mA 的外部电池供电。用那种电源,车子连 5 分钟都跑不了,更别说撑过整个派对了!

所以,你需要再剪断一根电缆,将其插入另一个 Grove 插槽,并将地线和 VCC 连接到你汽车的实际电源上。由于这辆车需要“承载”大量硬件,我选择了我儿子收藏的一辆吉普车(虽然他对库存减少并不太高兴,但看在可以手机控制和视频拍摄的份上,他还是妥协了)。

为了实现汽车的自动导航并增加避障能力,我们添加了一个红外传感器(亚马逊链接)。它有三个引脚:Vcc、Gnd 和 Vout。Vout 输出模拟电压,该电压会随着障碍物与模块距离的变化而变化。当障碍物靠近时,电压会更高。由于这是一个传感器,我们需要将其连接到 Grove 的模拟端口。所以,再剪一根线缆,将红色线连接到模块的 Vcc,黑色线连接到模块的 Gnd,黄色线连接到模块的 Vout。将模块调整到汽车前部。将你的 Edison 板、带有射频发射器的晶体管阵列板(被破解的电路)和一个 12V 电池调整到你的车上。

它看起来像下面这样。

图 5.1 在我们的车上安装红外传感器、电池、Edison 和遥控器破解模块后的遥控车

这个模型看起来不太精致,那是因为在这次比赛期间我没有太多时间来打磨它。希望你们中有人能用好的机电零件创造出一辆更好的车。

现在,我们需要在其中实现一个避障逻辑。为了避障,我们还必须能够控制汽车的速度。如你所见,汽车尾部很重,高速倒车可能会导致汽车失衡。我们将前进和后退晶体管与 D5 和 D6 连接,以便我们可以对电机使用 PWM。

当有障碍物靠近时,传感器会返回一个高值。一个理想的应用场景是在检测到障碍物时停下车。但请记住,我们希望我们的派对能在 YouTube 上直播。派对上会有很多人,我们不希望车停下来。我们希望它在检测到障碍物时能改变路线。我们用下图来解释这个概念。

图 5.2:车辆在检测到障碍物时的路径规划

汽车持续监测红外传感器的值,当值超过阈值时,它会检测到障碍物。一旦检测到障碍物,它会停止并启动一个“后退+右转”的序列。然后它会前进。由于之前的右转,它现在会与物体平行,但有侧向偏移。稍微向右转,然后再向左转以拉直车身。然后它继续前进。它也最好有一个后置传感器,但我在为这辆车做实验时只收到了一个。

消息交换架构在图 5.3 中有解释。

图 5.3 汽车自动导航的消息交换序列

现在我们需要对我们的代码做一点修改,以整合这个消息交换结构。

这是我们修改后的遥控代码。

var mqtt    = require('mqtt');
var client  = mqtt.connect('mqtt://192.168.1.9');
var mraa = require('mraa')
var SPEED=1
////////////////// Remote Pins//////////////////
var fPin = new mraa.Gpio(6)

var bPin = new mraa.Gpio(5)
var rPin = new mraa.Gpio(3)
var lPin = new mraa.Gpio(4)
var sensor=new mraa.Aio(0);

//////////////Set up Pins/////////////////////////
fPin.dir(mraa.DIR_OUT)
bPin.dir(mraa.DIR_OUT)

rPin.dir(mraa.DIR_OUT)
lPin.dir(mraa.DIR_OUT)
//////////////// Initialize///////////////////
fPin.write(0)
bPin.write(0)
rPin.write(0)
lPin.write(0)

/////////////////////////////////
client.subscribe('rupam/data/#')
client.handleMessage=function(packet,cb)
{
var payload = packet.payload.toString()
 if (payload === 'FORWARD') 
 {

 State=1;
 bPin.write(0);
 fPin.write(SPEED)

 } 
 if (payload === 'REVERSE')
{

 fPin.write(0);
 
 bPin.write(SPEED);

}
 if (payload === 'RIGHT')
 {
lPin.write(0);
 rPin.write(1)

 }
 if (payload === 'LEFT')
 {
rPin.write(0);
 lPin.write(1)

 }
 if (payload === 'STOP')
 {
 fPin.write(0)
 rPin.write(0)
bPin.write(0)
lPin.write(0)

 }
 if (payload === 'NO')
 {
 rPin.write(0)
 lPin.write(0)
 }

 


 
console.log(payload)
cb()
}

Loop();
var numRight=0;
var Obstacle=0;
var State=0;
function Loop()
{
setTimeout(Loop,100);
var a=sensor.read();
if(a>100)
{
  if(Obstacle==0)
  {
   Obstacle=30;
   fPin.write(0);
   //client.publish('rupam/data/','STOP');
  }
//client.publish('rupam/data/','RIGHT');
//numRight++;
}
else
{
 if(numRight>0)
  {
   client.publish('rupam/data/','LEFT');
   numRight--;
  }
}
if(Obstacle>1)
{
 Obstacle--;

  
}
if(Obstacle==1)
{
  Obstacle--;
  if(State==1)
  {
    State==0;
    client.publish('rupam/data/','REVERSE');
    client.publish('rupam/data/','LEFT');
    setTimeout(func, 2000);
       function func() {
           client.publish('rupam/data/','FORWARD');
           client.publish('rupam/Data/','RIGHT');
        
           setTimeout(f,200);
               function f()
                  {
                    client.publish('rupam/data/','NO');
                  }    
        }
  }
}
console.log(a);
}

6. 从 YouCar 到 YouTube 的直播

首先,你需要按照生物识别锁文章中这一部分的详细说明来配置你的摄像头。

对于 YouTube 直播,我们将使用以下架构:

图 6.1:视频流传输至 YouTube 的流程图。

首先,你需要在 Intel Edison 上安装 mjpg-streamer。

opkg install mjpg-streamer     

现在在一个 SSH Edison shell 中运行 streamer。

mjpg_streamer -i "input_uvc.so -y -n -f 30 -r 320x240" -o "output_http.so -p 8090 -n -w /www/webcam" 

Edison 将开始在端口 8090 上进行视频流传输。你可以先在浏览器中测试,看是否能接收到视频流。

在现代浏览器(如 Chrome)中使用以下 URL。

http://192.168.1.4?action=stream

将指定的 IP 地址替换为你的 EdisonIP 地址。

你将会看到来自你的车载摄像头的连续视频流。

现在,进入你的 YouTube 账户。点击右上角的个人资料图标,然后进入创作者工作室(注意,你需要有一个经过验证的 YouTube 账户才能进行直播)。

图 6.2 创作者工作室

图 6.4 在 YouTube 中选择直播

在左侧,选择直播选项。你的直播流将会打开。在底部,你会看到 RTMP 流的链接和一个密钥。复制这个密钥。你所需要做的就是用给定的密钥将任何视频流传输到这个链接。

 

请注意,我们的视频帧来自远程的 Edison 小车。mjpg 的一个问题是它只传输视频而不传输音频,而 YouTube 不允许没有音频的直播流。

所以,在将视频发送到 YouTube 之前,必须为传入的视频流添加一些音频并重新编码视频。

像往常一样,对于视频混合、音频捕捉以及其他视频-音频相关的任务,我们将使用明星级的 ffmpeg

因为我们需要捕捉并混合一些音频到我们的流中,所以首先找出音频录制设备的列表。

ffmpeg.exe -list_devices true -f dshow -i dummy

图 6.4 音频设备列表

你的麦克风会显示为类似绿色标记矩形框中的内容。复制它。

你可以使用以下命令测试录音:

ffmpeg.exe -f dshow -i audio="Microphone (Realtek High Definition Audio)" outputAud.wav

最后,在 ffmpeg 中使用最后一条命令,就能确保将你的 Edison 小车的视频帧流式传输到 YouTube。

ffmpeg.exe -f dshow -i audio="Microphone (Realtek High Definition Audio)" -f mjpeg -r 8    -i http://192.168.1.4:8090/video.jpg -acodec libmp3lame  -ar 44100 -b:a 128k  -profile:v baseline -s 320x240 -bufsize 2048k -vb 400k -maxrate 800k -deinterlace -vcodec libx264 -preset medium -g 30 -r 30 -f flv "rtmp://a.rtmp.youtube.com/live2/YOUR_KEY"

在上述命令中,你唯一需要更改的是你 Edison 的 IP 地址和你的密钥(YOUR_KEY)。你也可以将 Edison 配置为 640x480,这种情况下,在上述命令的 profile 部分,你必须将 320x240 改为 640x480。

大约 10 秒后,你的直播流将开始出现在 YouTube 上,如图 6.5 所示。

图 6.5 YouTube 上的直播。

我直播了其中一场英特尔活动(在印度班加罗尔举行的英特尔软件创新者大会)。你可以在这里观看录制的直播:英特尔软件创新者大会 - Edison 直播(现在是录制版本,因为活动和直播都结束了!)。

 

就是这样了。搭建好 YouCar,用手势/智能手机/语音控制它,并向 YouTube 直播活动。

这里有一些我用不太高清的相机拍的、不太专业的照片。但无论如何,它们能让你了解我们用完整的物联网和手势集成所打造的这个破解项目和这辆酷炫的小车。

 

7. 结论

我大约一年前开始这个项目,只是为了通过破解遥控车给我儿子带来一些乐趣。事实上,我大约在同一时间开始从“嵌入式物联网”转向纯物联网。英特尔非常慷慨地为我提供了套件来做一些很酷的事情。像英特尔的 Syed、Wendy Boswell 和 Sourav Lahoti,以及另一位英特尔黑带和挚友 Abhishek Nandy 这样的人帮助我突破了 Edison 板的能力极限和我的物联网知识。在过去的几个月里,我与我的妻子 Moumita 合作,已经构建了几个项目和原型。在这些人的帮助下,一辆简单的遥控车被开发成了一个功能齐全的机器人,具备了许多对商业机器人来说都至关重要的实时功能。Intel Edison-YouTube 广播器实际上是一个独立的项目。但我想实现一个复杂的软硬件堆栈集成,以测试英特尔物联网特性的能力。

虽然这个版本存在一些性能问题,比如电池耗电非常快,手机靠近时会出现抖动,但我相信这可以为你下一个为你孩子做的物联网项目,或者一个你想向朋友展示的项目提供一个好的开始。

我希望你享受这个项目,就像我享受制作它的过程一样。请留下你的建议/批评(或赞赏 :) )的评论。

 

© . All rights reserved.