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

fHealth - 用于基于活动的智能生活气候控制的雾计算活动跟踪框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (6投票s)

2016年8月7日

CPOL

24分钟阅读

viewsIcon

21502

downloadIcon

554

在边缘进行人类活动跟踪和聚合,进行基于活动的温度控制。

 

1. 概述

物联网(IoT)优于传统嵌入式系统的一个领域是云计算。核心中运行的大量服务为微型物理设备提供了巨大的计算能力。物联网的核心在于数据,更多的数据,以及数据的处理、分析和决策。

利用云计算的这一能力以及可用于数据收集、处理、分析和机器学习的广泛服务,一个新领域正在不断涌现——那就是医疗保健。

现在有各种各样的健身追踪设备,可以从用户那里收集活动数据,将其发送到云端,分析并提供其健康状况的统计概览。

但随着设备数量和用户数量的增加,数据量超出了想象。数据流量突破所有记录,数据的安全性和用户个人信息的保护成为一个问题,计算成本也越来越高。

近期,为应对这些挑战,一种新的架构概念应运而生,称为雾计算。这本质上是一种参考架构,提供网络边缘的数据存储、分析和决策。

思科(CISCO)的这份白皮书,题为“雾计算与物联网:将云延伸至物之所在”,可以让你很好地了解一些主要IT巨头是如何开始思考可以在网络边缘运行的服务。

这种架构理念相当简单:在网络边缘(本地网络)处理来自传感器设备的数据,在本地过滤和处理数据并做出可在本地进行的决策。然后,解释和聚合的数据(如有必要)将发送到云端。这减少了数据流量,降低了决策延迟,并使用户免受数据丢失的常见隐私问题困扰。

 

思科的论文让我最近开始思考这个领域,我对整个概念非常感兴趣。如果你可以在本地网络中运行一套实用服务,那么你可以根据用户需求、地理位置或其他因素对其进行定制。医疗保健和活动追踪是物联网领域中一个引起广泛关注的领域,自然也引起了我的关注。

因此,我想在医疗保健/健身追踪解决方案的背景下探索物联网的雾计算领域。

在我研究该方向的最新进展时,我意识到目前大多数健身应用都是基于云的应用。无论是Basis Peak、Fit Bit还是其他任何健身追踪器,设备都收集用户运动数据,将其本地存储,然后将数据发送到后端云端,进行处理并向用户提供其活动摘要。

当你购买健身追踪小工具或使用健身追踪应用程序时,应用程序会显示你当前的活动,如步行、慢跑、游泳等,以及你进行该活动的时间和可能的卡路里消耗。

一旦数据发送到云端并经过分析,聚合分析就会变得可用,这或多或少地提供了“指导”,例如这个人的健康状况如何,他需要多少睡眠,他需要多少步行等等。

这种架构通常采用的策略之一是将健身追踪与移动伴侣应用捆绑在一起。该应用从设备收集数据,可能会或可能不会对数据进行一些原始操作,最终将数据推送到云端。

因此,如果已经有一个负责从可穿戴健身设备收集数据的边缘层在工作,为什么不在边缘本身开发一个架构呢?

由于目前大多数健身设备都与其各自的云连接,我不得不从活动追踪层面的基础开始设计。由于我的主要目标是创建一个“基于雾的活动追踪”架构,我想使用Android手机,并将其用作活动追踪设备。不幸的是,允许Android手机用作健身设备的谷歌健身API也与谷歌云连接,这使得它无法使用。因此,整个问题被扩展为几个不同的问题集。

  • 在不使用任何基于云的API的情况下,直接在Android手机上创建基于Android的活动分类
  • 将活动日志发送到本地服务
  • 在本地服务中聚合活动数据,最后
  • 在本地服务中做出决策

现在,接下来的问题是哪种决策可以与本地活动追踪架构相关联。如果您正在创建活动追踪,您希望算法在提供给用户之前是稳健、经过良好测试和认证的。因此,在边缘开发像睡眠障碍这样的关键医疗决策检测有点困难。正是在我研究的这个阶段,我偶然发现了一篇精彩的研究论文,题为《基于位置的人体自适应空调在智能家居中的代谢率估算》[IEEE]。

本文采用基于距离传感器的方法跟踪房间内的人体运动,并利用累积数据控制空调。

我想“如果我的空调能根据我正在进行的工作量来设置温度,那该多好?”例如,如果我进行大量的体力活动,我将燃烧更多的卡路里,提高体温。另一方面,如果我正在休息,体温正常,我需要的冷却就会减少。

因此,这样的系统需要一个活动追踪应用程序来告知其活动性质及其对所需体温的影响。这引出了我们将在这里展示的工作:fHealth

本文不仅将介绍fHealth的理念,还将探讨如何着手设计这样一个分布式系统以及如何原型化这一理念。

拥有大量的iHealth-care、eHealth-Care、mHealth-Care、cHealth-Care系统后,现在是时候推出更个性化的框架了,那就是fHealth - 基于雾的医疗保健系统,我们将通过将活动追踪应用与智能气候控制系统集成来演示。继续阅读!

 

2. 架构设计

 

图2.1 fHealth系统架构

整体设计包含本地服务网格,该网格消耗来自Android活动追踪应用的数据。然后,这些数据被推送到一个决策服务,在此例中,它将是一个C# UI客户端,用于处理数据并决定所需的温度(此后被视为所需的风扇速度)。该决策被发送到连接到雾的Intel Edison。该设备连接了一个温度传感器,根据温度值和从决策单元发送的所需风扇速度,控制一个象征气候控制单元的风扇。

如您所见,象征性气候控制系统是一个附加组件,由本地决策系统做出的决策控制。本地决策(本质上是聚合的活动数据)可用于广泛的应用,包括“懒惰警报”等系统。

图2.2 框架的详细组件

图2.2展示了项目的详细组件。用户携带手机,加速度计数据被追踪。分类器在手机上实现,根据传感器数据将用户活动分类为以下类别:

  • 步行
  • 运行
  • 抖腿
  • 休息

这些数据被发送到本地Mqtt服务器(因为参考需要本地服务来处理连接)。然后,这些数据由用C#实现的分析系统消费。这可能会为用户生成警报,或者像2.1中那样,将其发送到控制环境参数(如温度)的物理设备。

 

在编程方面,我们需要在Android中创建一个活动追踪器,一个本地Mqtt代理,一个C#客户端作为决策系统,一个用于气候控制的Node.js Intel Edison应用程序。让我们在接下来的章节中逐一讨论这些主题。

3. 活动追踪 Android 应用

图3.1 Android Studio activity_main 布局

所提议的 Android 原型应用程序是一个单一的“表单”,单一布局的应用程序。此应用程序的目标是定期收集传感器数据,对其进行分类,然后通过本地云将其发送到本地服务。我们使用 Mqtt 作为网关协议。布局有一个名为 edServer 的 EditText 和一个用于连接到服务器的按钮 btnConnect

布局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="Activity Classifier Topic: rupam/activity]" android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tvHello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="MqTT Server"
        android:id="@+id/tvServer"
        android:layout_marginTop="56dp"
        android:layout_below="@+id/tvHello"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/edServer"
        android:width="200dp"
        android:layout_alignTop="@+id/tvServer"
        android:layout_toRightOf="@+id/tvServer"
        android:layout_toEndOf="@+id/tvServer"
        android:text="192.168.1.3" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Connect"
        android:id="@+id/btnConnect"
        android:layout_below="@+id/edServer"
        android:layout_centerHorizontal="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Status"
        android:id="@+id/tvStatus"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

您需要首先将 Mqtt 库添加到项目中。我们使用Eclipse 的 paho java 客户端作为 Mqtt 库。选择图 3.2 所示左上角的 Project 选项卡,然后将下载的 jar 文件拖到应用程序内的 lib 文件夹中。如果您没有看到 lib 文件夹,则从资源管理器中进入您的项目目录,进入 app 目录并创建一个名为 lib 的文件夹。现在将 jar 文件复制到那里。

 

3.2 将 Mqtt Paho jar 文件添加到项目中

现在右键单击项目选项卡中的jar文件,并将其添加为库。

由于我们将在应用程序中访问网络功能,因此您需要用户的权限才能访问网络服务。在<application>标签之前,在uses-permission部分添加"android.permission.INTERNET"。添加android:screenOrientation="portrait"以将布局固定为纵向。所有传感器分类均基于用户将手机垂直放置的考虑。它还可以防止应用程序在屏幕旋转时重置。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="in.co.integratedideas.activityclassifier" >
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

现在,在MainActivity.java中实现SensorEventListener。这要求你实现onSensorChanged方法。

在 Activity 类中声明 Sensormanager 类对象。

private SensorManager sensorManager;

在onCreate方法中,注册加速度计传感器事件以供sensorManager使用。这会自动为加速度计数据中的每次更改触发onSensorChanged事件监听器。

onCreate 方法中,还初始化 edServerbtnConnect 成员。为 btnConnect 添加事件监听器,以便每次点击时,应用都连接到 edServer 中指定的 Mqtt 代理。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

    sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), sensorManager.SENSOR_DELAY_UI);
    edServer=(EditText)findViewById(R.id.edServer);
    btnConnect=(Button)findViewById(R.id.btnConnect);
    tvStatus=(TextView)findViewById(R.id.tvStatus);
    btnConnect.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Connect();;
            broker="tcp://"+edServer.getText().toString().trim()+":1883";
            ConnectionClass con=new ConnectionClass();
            con.execute();
        }
    });

}

由于 Paho java Mqtt 客户端要求代理地址具有 tcp:// 前缀,因此在建立连接之前,请将其与 edSever 值拼接。

连接通过执行一个名为ConnectionClass的内部类来建立,该类扩展了AsyncTask。连接在类的doInBackground重写方法中作为后台进程建立。这是因为Android应用程序不允许在主线程中执行任何网络相关操作。

////////CONNECTION CLASS/////
public class ConnectionClass extends AsyncTask<ActivityGroup, String, String>
{
    Exception exc = null;

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

            //    sampleClient=new MqttClient(broker,clientId);
            MqttConnectOptions 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);
            //   Toast.makeText(MainActivity.this, "FAILED", Toast.LENGTH_LONG).show();
            // tv2.setText("Failed!!");
            return "FAILED";
        }
        // return null;
    }
    @Override protected void onPostExecute(String result) {
        super.onPostExecute(result);
        // Utils.usrMessage(getApplicationContext(), "Oh Noo!:\n"+exc.getMessage());
        //Utils.logErr(getApplicationContext(), exc);
        // finish();

        if(result!= null)
        {
            isConnected=true;
            tvStatus.setText(result);
            // setContentView(result);
        }
    }
}
boolean isConnected;

postExecute方法中,连接尝试完成后(无论成功或失败),我们将连接状态放入tvStatus中,tvStatus是一个TextView对象,用于记录任何状态消息,包括打印当前检测到的活动、Mqtt操作结果、连接等。

sampleClient是Mqtt客户端,用于同步发布消息到Mqtt代理并接收任何订阅通道的发布数据。

一旦应用程序被触发并建立连接,传感器数据会持续变化,触发onSensorchanged事件处理程序。让我们进入事件处理程序,看看如何根据加速度计数据对用户活动进行分类。

float prevX=-1,prevY=-1,prevZ=-1;
float DX=0;
long lastUpdate = System.currentTimeMillis();
int WAIT=1000;
boolean detected=false;
@Override
public void onSensorChanged(SensorEvent event) {

    String command="";
    if(!isConnected)
    {
      //  return;
    }
    float[] values = event.values;

    // Movement
    float x = Round(values[0], 1);
    float y = Round(values[1], 1);
    float z = Round(values[2], 1);
    command="";
    long actualTime = System.currentTimeMillis();
    if ((actualTime - lastUpdate) > WAIT)
    {

        long diffTime = (actualTime - lastUpdate);
        lastUpdate = actualTime;
 boolean sit_stand_sleep=false;

        ///// Make the Calculations Here//////
        float diffX=x-prevX;
        float diffY=y-prevY;
        float diffz=z-prevZ;

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

        double xz=Math.abs(diffX)* Math.abs(diffz);
        double xyz=Math.abs(diffX)+Math.abs(diffz)+Math.abs(diffY);
        if(xz>1.1 )
        {
            if(xz>40)
            {
                Log.d("RECOGNIZED", "RUNNING "+xz);
                command="RUNNING_"+xz;
            }
            else {
                Log.d("RECOGNIZED", "WALKING " + xz);
                command="WALKING_"+xz;
            }
            sit_stand_sleep=true;
        }
        else
        {
            if(xz>.1)
            {
                Log.d("RECOGNIZED", "SHAKING LEGS");
                command="SHAKING LEGS";
            }
            else
            {
                if(xyz<.3)
                {
                    Log.d("RECOGNIZED", "MOBILE NOT WITH USER");
                    command="MOBILE NOT WITH USER";
                }
                else {
                    Log.d("RECOGNIZED", "RESTING");
                    command="RESTING";
                }
            }

        }
        /// Finally Update the past values with Current Values
        if(prevX!=-1)
        {



            DX=DX+diffX;


            prevX = x;
            prevZ = y;
            prevZ = z;

        }
        else
        {

            prevX = x;
            prevY = y;
            prevZ = z;

        }

            if(command.length()>1)
            {

                Send(command);
                command = "";
            }

    }
    ///////////////
}

我们的假设基于以下观察:每当用户移动时(即使是轻微的移动),传感器数据都会发生变化。如果手机不在用户身边(放在桌子上),xyz将完全没有移动。这会导致xyz乘积接近零。如果观察到此值极低,系统将生成消息“手机不在用户身边”。

图3.3:手机垂直放置时的X-Y-Z方向(来源:stackoverflow

当手机垂直放置时(参见图3.3),用户的任何移动都会导致x和z的显著变化。如果用户处于休息位置,传感器数据仍会发生变化,但变化幅度会比手机闲置时大,但比用户实际移动时小得多。这就是检测RESTING和“手机不在用户身边”活动场景的方式。

if(xyz<.3)
                {
                    Log.d("RECOGNIZED", "MOBILE NOT WITH USER");
                    command="MOBILE NOT WITH USER";
                }
                else {
                    Log.d("RECOGNIZED", "RESTING");
                    command="RESTING";
                }

当用户移动非常快时(因为在室内环境中,严格来说没有所谓的跑步情况),XZ变化非常显著。我们利用这种变化,将XZ显著高的活动归类为跑步。

如果xz值大于抖腿情况但小于跑步情况,则为步行。引入抖腿是为了检测用户进行中等移动的工作,例如阅读书籍或绘画。

 if(xz>40)
            {
                Log.d("RECOGNIZED", "RUNNING "+xz);
                command="RUNNING_"+xz;
            }
            else {
                Log.d("RECOGNIZED", "WALKING " + xz);
                command="WALKING_"+xz;
            }

因此,通过使用模糊规则集,我们将活动分类为步行、跑步、休息、抖腿和手机未随身携带。

有趣的是,您可能已经注意到,当您走得很慢时,出汗较少;当您走得快时,出汗较多。逻辑是,代谢率(最终导致卡路里消耗和体温变化)不仅取决于活动,还取决于活动的强度。

因此,对于步行和跑步,我们也嵌入了活动的强度。由于休息总是导致接近零的额外卡路里损失(除了身体由于正常生物活动燃烧的卡路里),我们不将任何强度值纳入此活动,也不纳入抖腿活动。

参数xz和xyz是它们各自加速度计数据与当前值和先前值的差值乘积。

 

 double xz=Math.abs(diffX)* Math.abs(diffz);
        double xyz=Math.abs(diffX)+Math.abs(diffz)+Math.abs(diffY);

计算差值如下。

 float diffX=x-prevX;
        float diffY=y-prevY;
        float diffz=z-prevZ;

您可能已经注意到,最终分类的活动存储在一个名为command的String变量中。

活动分类完成后,应用程序检查与代理的连接,如果应用程序已连接到代理,则将活动数据发布到代理。

 if(command.length()>1)
            {

                Send(command);
                command = "";
            }

图3.4:运行应用程序的截图

上图显示了正在运行的应用程序的屏幕截图。您还可以在调用Send()方法之前添加tvStatus.setText(command),以显示当前活动。上面的屏幕显示了我的测试屏幕。服务器(技术上是代理)地址是我们的本地Mqtt代理的地址。

Send方法检查sampleClient是否连接。如果连接,则发布命令。

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

            try
            {
                if(sampleClient.isConnected())
                {
                    MqttMessage message = new MqttMessage(data.getBytes());
                    message.setQos(qos);
                    sampleClient.publish(topic, message);
                }
                else
                {

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

            }
            //TODO your background code
        }


    });
}

4. Mqtt 代理

我们已在基于足部手势的文本输入系统教程中发布了在 Windows 中安装和运行 Mqtt 代理和实例的过程。为了文章的连贯性,该部分在此处重现。

 

为了测试应用程序,您可以使用iot.eclipse.org。但是,由于代理是远程的,数据通信存在明显的延迟。基于足部手势的文本输入系统本身就比较耗时。所以,您肯定不想再增加远程服务器的网络延迟,对吗?

所以,我们将使用Windows上的本地Mqtt代理。Windows只允许1063个连接(我不知道为什么)或客户端。但是,由于我们只连接一个客户端到我们的代理,在本地机器上运行的Windows Mqtt服务器将足够使用。

您需要先安装Win32 Open SSL

现在下载并安装Windows 版 Mosquitto 代理

现在进入OpenSSL安装目录,该目录应位于C:\OpenSSL-Win32,然后复制pthreadVC2.dll文件,并将其替换到Mosquitto安装目录中的同名文件。就这样。

从命令提示符进入Mosquitto目录,运行mosquitto.exe即可查看代理在您的系统中运行。要关闭代理,请使用ctrl+c。代理的IP地址将是您PC的本地IP地址,您可以使用ifconfig命令获取。

 

5. C# 客户端用于分析活动并决策所需温度

首先,我们定义一个活动类。一个活动将具有开始时间、结束时间、名称和强度。休息和抖腿的强度为0(这意味着它们的强度不会影响温度)。

开始时间是当前活动开始的时间。结束时间是活动结束的时间。当用户处于特定状态时,例如休息,活动将有一个开始时间,结束时间将为空。当用户改变活动时,例如从休息到步行,休息的结束时间将被记录,并以步行的新名称开始一项新活动,并记录其开始时间。

由于步行和跑步都有强度值,所以只要当前强度有任何变化,结束时间就会被标记,并创建一个具有新强度的另一个步行实例。

public class Activity
    {
        public String Action { get; set; }
        public string Start { get; set; }
        public string Stop { get; set; }
        public string TotalPeriod { get; set; }
        public double Intensity { get; set; }
        public Activity()
        {
        }
        public Activity(string Action, string Start, string Stop, string TotalPeriod, double Intensity)
        {
            this.Action = Action;
            this.Start = Start;
            this.Stop = Stop;
            this.TotalPeriod = TotalPeriod;
            this.Intensity = Intensity;
        }
    }

因为开始时间和结束时间不总是定义的,我们声明StartStop为String而不是DateTime。

我们定义了一个活动类列表,用于记录连续的活动实例。

 List<Activity> lv = new List<Activity>();

A StopWatch instance is created to keep track of the TotalPeriod of the ongoing activity.
 Stopwatch st = new Stopwatch();

该应用程序是一个单一的表单应用程序,其行为或多或少像一个服务。该应用程序消费活动追踪移动应用程序发布的活动数据,聚合数据,做出决策,并作为全局网关发布演示,将数据发布到iot.eclipse.org(一个外部代理)。我们的Edison服务连接到iot.eclipse.org,使整个雾系统成为本地运行和云端服务的组合,相互协作以提供完整的解决方案。

当应用程序启动时,通过调用LocalIPAddress()方法获取PC的IP地址,该方法遍历所有DNS条目并查找地址表中的IP地址条目。在PC上运行的Mqtt代理将具有与PC地址相同的IP地址。因此,获取和显示IP地址使用户可以轻松地从其Android活动追踪应用程序连接到代理。

图5.1:C# 应用用户界面

  public string LocalIPAddress()
        {
            IPHostEntry host;
            string localIP = "";
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    localIP = ip.ToString();
                    break;
                }
            }
            return localIP;
        }
        private void Form1_Load(object sender, EventArgs e)
        {
         
            labIP.Text = LocalIPAddress();
        }

请注意,由于Android应用程序和C#应用程序在雾框架内协作,两者必须属于同一个WiFi网络。

请注意,主要有两个按钮,标签分别为“启动代理”和“连接到代理”。基本上,您可以从命令行启动Mosquitto代理。但由于我们将C#应用程序更多地视为雾服务,我希望从我们的应用程序本身启动服务器并将其附加到窗口。

使用 .Net 的 Diagonestic.Process() 类启动外部服务非常简单。

SetParent() 方法用于将分叉进程的窗口句柄附加到 UI 组件。我们还会在启动新 Mosquitto 服务之前杀死 PC 上所有已运行的 Mosquitto 服务,因为我们已将 1883 假定为 mqtt 代理的默认端口。如果代理已在运行,则无法启动新的代理实例,因为它无法获取所需的空闲端口。

 

 [DllImport("user32.dll", SetLastError = true)]
        static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
        private void button1_Click(object sender, EventArgs e)
        {
            string s=Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
            s = s + "\\mosquitto\\mosquitto.exe";
            //System.Diagnostics.Process.Start(s);
            procMqtt = new System.Diagnostics.Process();
            procMqtt.StartInfo = new System.Diagnostics.ProcessStartInfo(s," -v");
            procMqtt.StartInfo.UseShellExecute = true;
            Process[] pname = Process.GetProcessesByName("mosquitto");
            for (int i = 0; i < pname.Length; i++)
            {
                pname[i].Kill();
            }
           
            procMqtt.Start();
            System.Threading.Thread.Sleep(2000);
            SetParent(procMqtt.MainWindowHandle, groupBox2.Handle);
           
        }

 

一旦代理实例启动,表单将类似于图5.2

图5.2 从应用程序启动代理实例并将其附加到分组框

回想一下,在我们的案例中,我们使用了两个 Mqtt 代理:一个是您从应用程序启动的本地代理。此代理是本地活动追踪服务和 C# 客户端之间的雾接口。

另一个代理是 iot.eclipse.org,用作全局代理。C#客户端使用Eclipse网关向Edison设备发送所需的温度级别。这绝不是必需的,您可以将整个架构连接在本地环境中。但由于按设计,雾系统应将分析数据传输到全局云,因此我们使用了两个不同的网关。

因此,我们的PC C#应用程序有两个Mqtt客户端:mc和mqSpeedSender。mc连接到您从应用程序启动的本地代理,其IP地址将是PC的本地IP地址;mqSpeedSender的地址将是iot.eclipse.org。

您可以在图5.1和5.2中观察到我的系统显示的IP地址是10.0.x.y。但是,本地IP地址的格式将是192.168.x.y。这是因为我安装了Gulp,以便在将我的某些Python应用程序通过Cloud Foundry推送到AWS之前,对其进行全局测试。然而,根据定义,雾服务必须在本地环境中运行。因此,为了获得准确的本地地址,您可以更改LocalIPAddress()方法,并检查地址的第一个字节为192的条目,如下所示。

 public string LocalIPAddress()
        {
            IPHostEntry host;
            string localIP = "";
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    if (ip.ToString().Contains("192"))
                    {
                        localIP = ip.ToString();

                        break;
                    }
                }
            }
            return localIP;
        }

一旦完成,您将看到您的表单显示本地IP地址,这是雾框架的先决条件。如果您看到类似以下内容的IP地址,那么您就一切就绪。

图5.3:修复LocalIPAddress()方法后的正确本地IP地址

现在,当点击“连接到代理”按钮时,应用程序必须使用名为mc的Mqtt客户端实例连接到分叉代理,并使用名为mqttSpeedSend的另一个实例连接到全局Eclipse代理。

MqttClient mc = null;
        MqttClient mqSpeedSender = null;
        System.Diagnostics.Process procMqtt;
        private void button2_Click(object sender, EventArgs e)
        {
            try
            {
                var ip = IPAddress.Parse(labIP.Text);
                mc = new MqttClient(ip);
               
                mc.Connect("RUPAM");
                mc.Subscribe(new string[]{topic},new byte[]{(byte)0});
                mc.MqttMsgPublishReceived += mc_MqttMsgPublishReceived;
                mc.MqttMsgSubscribed += mc_MqttMsgSubscribed;
                
                MessageBox.Show("Connected");
                mc.Publish(topic, GetBytes("VM Broker Started"));
                for (int i = 0; i < 4; i++)
                {
                    chart1.Series[i].Points.AddY(0);
                }
                /// Connect the sender
                /// 
                mqSpeedSender = new MqttClient("iot.eclipse.org");
                mqSpeedSender.Connect("RUPAM");
            }
            catch
            {
            }
        }

连接到代理后,您将在代理窗口中看到消息交换,因为我们使用 -v(详细)选项启动了代理。

图5.4:已建立与代理的连接

请注意,我们为本地代理 mc 添加了一个消息接收事件处理程序。

  mc.MqttMsgPublishReceived += mc_MqttMsgPublishReceived;

每当有新消息到达(来自Android活动追踪应用程序的活动数据)时,都会调用此事件处理程序。

这是我们的主要分析方法。我们在这里执行多个任务。让我们先看看整个方法,然后我们将分解事件处理程序的逻辑部分并逐一解释它们。

  void mc_MqttMsgPublishReceived(object sender, uPLibrary.Networking.M2Mqtt.Messages.MqttMsgPublishEventArgs e)
        {
            //throw new NotImplementedException();
            //MessageBox.Show(GetString(e.Message));
            this.Invoke((MethodInvoker)delegate
            {
                if (e.Message[1] == (byte)0)
                {
                  //  listBox1.Items.Add(GetString(e.Message));

                }
                else
                {
                    try
                    {
                        string command = "";
                        for (int i = 0; i < e.Message.Length; i++)
                        {
                            //  command = command + ('A' + ((int)e.Message[i] - 64));
                            command = command + ((char)('A' + ((int)e.Message[i] - 65))).ToString();

                        }
                        //////
                        double intensity = 0;
                        String name = "";
                        if (command.Contains("_"))
                        {
                            string[] parts = command.Split(new char[] { '_' });
                            intensity = double.Parse(parts[1]);
                            name = parts[0];
                        }
                        else
                        {
                            name = command;
                        }
                        if (!command.Contains("MOBILE"))
                        {
                            if (lv.Count > 0)
                            {
                                if (lv[lv.Count - 1].Action.Equals(name))// if user is doing the same thing, just update timing 
                                {
                                    if (intensity == 0)
                                    {
                                        // Like resting.. user needs to to for the period he is resting
                                        TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                                        lv[lv.Count - 1].TotalPeriod = ts.ToString();
                                        
                                    }
                                    else
                                    {
                                        // every time intensity will differ. So need to log every instnce
                                        lv[lv.Count - 1].Stop = DateTime.Now.ToString();
                                        TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                                        lv[lv.Count - 1].TotalPeriod = ts.ToString();
                                        /// Add.
                                        var activity = new Activity(name, ""+DateTime.Now, null, null, intensity);
                                        lv.Add(activity);
                                        
                                    }
                                }
                                else// A new activity--- So pack up the old one and enter this one.
                                {
                                    // packup the last one
                                    lv[lv.Count - 1].Stop = DateTime.Now.ToString();
                                    TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                                    lv[lv.Count - 1].TotalPeriod = ts.ToString();
                                    //add the new one
                                    var activity = new Activity(name, "" + DateTime.Now, null, null, intensity);
                                    lv.Add(activity);
                                    
                                   
                                }
                            }
                            else// A new activity--- So pack up the old one and enter this one.
                            {
                              
                                //First Activity... Just Addadd the new one
                                var activity = new Activity(name, "" + DateTime.Now, null, null, intensity);
                                lv.Add(activity);
                            }

                            #region updating chart
                            try
                            {
                                var walk = lv.Where(x => x.Action.Equals("WALKING")).ToList();
                                var wt = walk.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var rest = lv.Where(x => x.Action.Equals("RESTING")).ToList();
                                var rt = rest.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var run = lv.Where(x => x.Action.Equals("RUNNING")).ToList();
                                var runt = run.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var shake = lv.Where(x => x.Action.Contains("SHAKING")).ToList();
                                var st = shake.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();
                                /*
                                chart1.Series[0].Points[0].YValues[0] = rt;
                                chart1.Series[1].Points[0].YValues[0] = st;
                                chart1.Series[2].Points[0].YValues[0] = wt;
                                chart1.Series[3].Points[0].YValues[0] = runt;
                                 */
                                chart1.Series[0].Points.AddY(rt);
                                chart1.Series[1].Points.AddY(st);
                                chart1.Series[2].Points.AddY(wt);
                                chart1.Series[3].Points.AddY(runt);
                                var speed = (wt + runt + st / 3) / rt;
                                speed = (double)(Math.Floor(speed * 100)) / 100.0;
                                speed = speed / 3.0;
                                var ss = speed;
                                if (speed > 1.0)
                                {
                                    speed = 1;
                                }
                                if (speed < .45)
                                {
                                    speed = .45;
                                }
                                labSpeed.Text = "Speed=" + speed;
                                try
                                {

                                    mqSpeedSender.Publish("rupam/Fan", GetBytesForEdison(speed.ToString()));
                                }
                                catch
                                {
                                }
                            }
                            catch
                            {
                            }
                            #endregion
                            chart1.Invalidate();
                            chart1.Update();
                            // calculate speed
                            
                            dataGridView1.DataSource = null;
                            dataGridView1.DataSource = lv;
                            dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.RowCount - 1;
                        }
                        
                    }
                    catch
                    {
                    }

                }
            });
        }

 

1. 将字节数据转换为字符串:请注意,Mqtt数据始终以字节模式发布。因此,我们需要将字节转换为字符串。

 

string command = "";
                        for (int i = 0; i < e.Message.Length; i++)
                        {
                            //  command = command + ('A' + ((int)e.Message[i] - 64));
                            command = command + ((char)('A' + ((int)e.Message[i] - 65))).ToString();

                        }

你可能会想到一个问题,为什么我们不能直接使用 Encoding.UTF8.GetString() 呢?事实上,我最初就是这样做的。后来我发现,许多设备,比如 Intel Edison,会为一个字符生成两个字节,一个字符字节后跟着一个 '\0'。GetString() 方法在这种情况下会失败。经过多次尝试和错误,我才意识到文本很多时候都是用 ASCII 编码发送的。我甚至无法从 ASCIIEncoding.GetString() 方法中获取字符串消息。所以,我最终选择了提取等效整数,减去 ASCII 值,提取字符索引,然后加上字符 'A'。这当然不简洁,但适用于所有 Mqtt 交换(已使用 Node.js、Python、Android 和另一个 C# 客户端发布的数据进行测试)。

2. 将活动存储在 lv 列表中

回想一下,当手机不在用户身边时,“MOBILE not with user”命令被发送。我们不想追踪此活动。因此,只有当命令不包含MOBILE关键字时,我们才进入分析循环。

现在,从逻辑上讲,从Android手机发送的活动可能与上一个活动相同(当用户持续休息时,每个周期性实例都会收到RESTING命令),我们不想创建新的日志条目。但是,当出现与上一个活动不同的新活动时,我们希望通过添加结束时间EndTotalPeriod来打包上一个活动。这是通过将lv(活动列表)的最后一个元素与当前活动进行比较来完成的。

if (lv.Count > 0)// If no previous entry, nothing to check
                            {
   if (lv[lv.Count - 1].Action.Equals(name))// if user is doing the same thing, just update timing 
                  {
                           if (intensity == 0)
                           {
                            // Like resting.. user needs to know  for the period he is resting
                            TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                            lv[lv.Count - 1].TotalPeriod = ts.ToString();
                                
                            }
                           else
                           {
                             // every time intensity will differ. So need to log every instnce
                             lv[lv.Count - 1].Stop = DateTime.Now.ToString();
                             TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                             lv[lv.Count - 1].TotalPeriod = ts.ToString();
                             /// Add.
                            var activity = new Activity(name, ""+DateTime.Now, null, null, intensity);
                            lv.Add(activity);
                                        
                            }
                     }

如果出现新活动,则打包上一个活动,创建一个将用新活动更新的空白活动。打包过程将包括更新结束时间(即当前时间)和计算总持续时间(即开始时间和结束时间之间的时间戳差)。

else// A new activity--- So pack up the old one and enter this one.
                                {
                                    // packup the last one
                                    lv[lv.Count - 1].Stop = DateTime.Now.ToString();
                                    TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(lv[lv.Count - 1].Start));
                                    lv[lv.Count - 1].TotalPeriod = ts.ToString();
                                    //add the new one
                                    var activity = new Activity(name, "" + DateTime.Now, null, null, intensity);
                                    lv.Add(activity);
                                    
                                   
                                }

3. 数据聚合:数据聚合本质上是一个求和过程,我们找出用户步行、跑步和抖腿的总时间,因为气候控制取决于这些参数。现在请注意,步行和跑步都附带一个强度值,我们希望将其纳入我们的决策。因此,我们对强度值使用30的比例因子(从观察中我们发现,当Android应用中的xz值约为37时(作为步行和跑步的强度值附加),用户以约1.2m/s的速度移动。因此,将xz值乘以30的因子即可获得用户的m/s速度)。

 var walk = lv.Where(x => x.Action.Equals("WALKING")).ToList();
                                var wt = walk.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var rest = lv.Where(x => x.Action.Equals("RESTING")).ToList();
                                var rt = rest.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var run = lv.Where(x => x.Action.Equals("RUNNING")).ToList();
                                var runt = run.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

                                var shake = lv.Where(x => x.Action.Contains("SHAKING")).ToList();
                                var st = shake.Select(x => TimeSpan.Parse(x.TotalPeriod).TotalSeconds + x.Intensity / 30).Sum();

因此,我们将强度值与活动执行的总时长相加。

4. 计算风扇速度(去模糊化):请注意,风扇速度与用户运动直接成比例,用户运动是总步行、跑步和抖腿的总和。它也与总休息时间成反比。因此,当用户长时间休息时,风扇速度应自动降低。我们还将速度值限制在0.45到1之间。通常情况下

期望温度刻度 = 总运动 / 总休息

var speed = (wt + runt + st / 3) / rt;
                                speed = (double)(Math.Floor(speed * 100)) / 100.0;
                                speed = speed / 3.0;
                                var ss = speed;
                                if (speed > 1.0)
                                {
                                    speed = 1;
                                }
                                if (speed < .45)
                                {
                                    speed = .45;
                                }

5. 更新图表:没有可视化功能的健身/活动追踪应用程序算什么?我们将聚合结果作为折线图添加到我们的MS Chart控件中。

 chart1.Series[0].Points.AddY(rt);
                                chart1.Series[1].Points.AddY(st);
                                chart1.Series[2].Points.AddY(wt);
                                chart1.Series[3].Points.AddY(runt);

6. 将所需速度发送至Edison:一旦分析方法计算出舒适水平风扇速度值(您可以将其用作所需空调值,并通过在Edison上添加红外遥控单元并实现空调制造商遥控器的协议来控制您的空调),它将发送到全局网关以演示雾系统的整个工作流程。

 

try
                                {

                                    mqSpeedSender.Publish("rupam/Fan", GetBytesForEdison(speed.ToString()));
                                }
                                catch
                                {
                                }

其中 getBytesForEdison() 是一个方法,用于移除任何多余的 '\0' (实际字符字节后添加的第二个字节)。

static byte[] GetBytesForEdison(string str)
        {
            byte[] bytes = new byte[str.Length * sizeof(char)];
            System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
            List<byte> newBytes = new List<byte>();
            for (int i = 0; i < bytes.Length; i++)
            {
                if (bytes[i] > 0)
                {
                    newBytes.Add(bytes[i]);
                }
            }
            return newBytes.ToArray();
        }

图5.5 结果解读

 

图5.5展示了活动追踪器如何响应用户活动,以及速度如何根据用户移动而变化。您只需启动您的Android应用程序,连接到代理并将其放在口袋中(垂直放置——就像裤子口袋一样),您将看到当您开始改变活动时线条会随之变化。

6. 基于英特尔爱迪生的风扇控制

图6.1 原理图

我们使用PWM来控制风扇速度。我们使用一个工作电压为5v的PC USB风扇,并按照上图所示连接电路。BC548通过Edison的PWM端口D5控制。假设您正在使用带有Edison的Grove Shield,您只需拿一根Grove连接线并从中间剪断。红色线是Vcc 5v,需要连接到风扇的红色线。剪断连接器的黑色线连接到BC548的第三个引脚(接地)。

风扇的黑线连接到BC548的第一个引脚。

我们将LCD连接到I2C端口以显示当前温度和速度,并将温度传感器连接到A0端口。

现在,当您从程序中改变PWM的占空比时,风扇速度会发生变化。占空比的这种变化是从我们的C#应用程序发送的,经过分析来自Android活动追踪应用程序的数据后。

var mraa = require ('mraa');
var tempPin = new mraa.Aio(2);
var LCD = require('jsupm_i2clcd');
var myLCD = new LCD.Jhd1313m1(6, 0x3E, 0x62);
var B=3975;   
loop();
var cnt=0;
var r=100,g=0,b=0;
var fanPin=new mraa.Pwm(6);
fanPin.enable(true);
var fanSpeed=0.65;
fanPin.write(fanSpeed);
var mqtt    = require('mqtt');
var client  = mqtt.connect('mqtt://iot.eclipse.org');
client.subscribe('rupam/Fan/#')
client.handleMessage=function(packet,cb)
{
var payload = packet.payload.toString()
console.log(payload.length);
payload = payload.replace(/(\r\n|\n|\r)/gm,"");
//console.log(payload.length)
console.log(payload)
try{
//payload=".9 "
fanSpeed=Number(payload.trim());
fanPin.write(fanSpeed);

}catch(ex)
{
console.log(ex.message);
}
cb()
}

function loop()
{
myLCD.clear();
 myLCD.setCursor(0,0);
myLCD.write("Temp: ");
myLCD.setCursor(0,6);
var tempValue=tempPin.read();
var resistance=(1023.0-tempValue)*10000.0/tempValue; //get the resistance of the sensor;
var temperature=1/(Math.log(resistance/10000.0)/B+1/298.15)-273.15;//convert to temperature via datashee

myLCD.write(""+temperature);
myLCD.setCursor(1,0);
myLCD.write("Fan Speed:");
myLCD.setCursor(1,10);
myLCD.write(""+fanSpeed);
if(fanSpeed<=.65 && fanSpeed>.5)
{
myLCD.setColor(255,0,0);
}
if(fanSpeed<.85 && fanSpeed>.65)
{
myLCD.setColor(255,255,0);
}
if(fanSpeed>=.85)
{
myLCD.setColor(0,255,0);
}

setTimeout(loop,1000);
}

 

每当Edison通过通道rupam/fan接收到数据时(建议您更改此通道以进行实验,因为许多读者可能正在尝试同一通道),它都会通过pwm的write()方法写入该值。

你可以开始走路,然后你会发现随着风扇速度的增加,温度会下降:)

6. 结论

在此项目中,我们为健身应用创建了一个雾计算参考架构。我们从头开始在 Android 中设计了自己的健身追踪器,没有使用任何库。尽管该追踪器非常基础,但我们发现它在正确检测活动方面非常高效。然后,我们创建了一个 C# 客户端,利用该语言的强大功能分析活动数据并将其转换为舒适的温度值。然后,这被发送到 Intel Edison,Edison 运行一个 Node.js 应用,从全局 Mqtt 网关获取速度并相应地改变风扇速度。因此,在此项目中,我们展示了如何在雾环境中结合 Android 伴侣应用、客户端 PC 应用和物联网应用。您可以在本地网络中部署许多此类服务,并创建实用服务链。这种架构允许您创建自己的智能世界,而无需依赖基于云的 API,也无需担心个人数据在云中泄露的风险。

保持健康,舒适生活,疯狂编码。

© . All rights reserved.