利用树莓派与安卓搭建监控系统

在本文中,使用一个简单的方法搭建了一个可以在局域网中使用的监控系统(应用:安卓遥控智能小车,并在安卓端查看实时图像)

所需软件

  1. mjpg-streamer:这是一个开源的推流工具,地址在这里,支持多种输入方式,可以直接读取摄像头,也可以使用opencv,在github中也说明了方法
  2. Visual Studio + Xamarin:在本文中,将使用Xamarin来进行安卓开发,使用原生的开发方式(例如Android Studio)也可以,方法类似

步骤

  1. 按照mjpg-streamer的文档的说明,开启推流,下面的例子是直接使用摄像头:
mjpg-streamer -o "/YOUR/PATH/TO/output_http.so" -i "/YOU\/PATH/TO/input_uvc.so"

注意一定要将YOUR/PATH/TO改为你自己的相应的路径~

成功执行上面的命令后,打开浏览器,输入http://localhost:8080/?action=stream就可以看到图像啦
http://localhost:8080/?action=snapshot则是获取一个截图

上面所说的是,在本机推流,所以如果要在树莓派上的话,就迁移到树莓派上即可。如果在树莓派上的话,就需要将localhost改为树莓派的ip地址

安卓端

由于mjpg-streamer已经将图像推到一个网页上了,所以在安卓端我们也无需费事,直接拖一个WebView即可,将WebView的Source属性设置为树莓派的IP地址即可~


以上

在线程中使用Toast以及其他有关UI更新的API

背景与声明

博主在写这篇文章的时候,使用的环境是Xamarin.Form,Xamarin.Android也适用。不过考虑到Xamarin.Form/Android会运行在Android环境中,所以本文内容也适用于原生Android开发。

但是与原生Android开发的不同点在于,Xamarin.From/Android使用的是C#语言,而原生Android开发使用的是Java/Kotlin,所以在语法、库以及编程习惯等多方面会有不同,请读者悉知。

序言

在Android中,为了良好的用户体验,是不允许在UI进程(主进程)中进行一些可能运行比较长的时间的操作的(例如网络操作和文件操作)。其原因也简单,如果在主进程中进行了这样的操作(例如写文件),可能会导致UI长时间无响应,进而影响用户体验。所以这样的操作一般要放在单独的进程中。

正文

我们现在解决了在UI中执行长时间操作的问题(具体方法待补充),但是如果我们如果需要在“长时间操作”中执行UI更新怎么办?直接在里面写吗?在下面的代码中,写了一个方法,而这个方法将会以线程的形式来执行:

private void Run()
{
    // Do something below.
        // ...
        // Make a Toast
    Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
        // Do something else.
        // Blah Blah ...
}

但是如果这样写了代码,我们就会遇到一个异常

Can't toast on a thread that has not called Looper.prepare()

这是为什么呢?在报错中提到的Looper.prepare()又是什么呢?

关于Looper类

Looper是一个类,主要用来为一个线程开启一个消息循环。在默认情况下,Android没有为每一个线程开启消息循环(关键是Android也不能确定我们的线程到底需不需要这个消息循环,如果万一我们不需要,而Android又为我们开启了,那岂不是浪费资源?)。

为什么要用Looper

更新UI(Toast也算是更新UI的一种),是需要主线程来做的。如果真的要子线程(也就是我们自己开启的线程)来做,可能会引起一系列的问题,况且Android也不允许我们这样做。实际上不仅Android中,在之前编写C#图形界面的时候,也有这种不允许子线程更新UI的情况。

所以如果我们需要更新UI,需要使用消息队列来处理这个操作。也就是说,我们需要把更新UI的操作串行化。具体的方法是使用一个消息队列。如果我们需要在子线程中更新UI,就要将一个更新UI的操作以一个消息(可以理解为请求)的形式发布出去,由管理UI的线程(主线程)来处理,至于到底执行不执行,以及如何执行,就是主线程的事了。

在C#中,处理的方法是使用一个委托来异步地执行UI更新操作,而在Android中则是使用一个消息队列,在更新UI前,准备好一个消息队列,然后执行操作,这样,要执行的UI更新操作就进入到了消息队列中,再由主线程来执行具体的操作。具体使用的方法是,在执行UI更新之前,调用Looper.prepare()方法。例如我们Toast一下:

// 前面的代码省略
Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();

这样就行了吗?答案是否定的,我们Prepare()了一个Looper那么下面的指令就会进入到队列里,但是我们还要说明,到哪里我们的代码就不进入队列了,方法是使用Looper.Loop()方法。完整的代码如下:

Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
Looper.Loop();

使用Looper的弊端

有一点要说明一下,在一个进程(或者是Handler)中,Looper.Loop()方法后面的代码是不会被执行到的,也就是说,如果有下面这样的代码:

Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
Looper.Loop();
// Some code below Looper.Loop()

那么// Some code below Looper.Loop()是不会执行的,所以这种方法的应用也比较有限。例如,我们在一个线程(或者Handler)中只需要在最后才需要更新UI,就可以使用这种方法。但是如果我们需要一直开一个进程(例如一直侦听一个端口),那么就不太合适了(实际上,如果我们如果真的有这种需求的话,会用Service来实现吧(* ̄︶ ̄))

不过既然提到了,就继续说吧~

使用Handler来实现

这个方法说到底还是使用消息队列,不过好处在于,我们不需要使用Looper,也就意味着,在更新UI的代码后面,我们还可以放其他的代码~

我们可以使用Handler来处理消息:

private Handler msgHandler = new Handler((Message msg) =>
{
        Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
});

上面的代码实际上是一个Action(这是C#里面的概念!)

而使用它的方法也很简单,假设下面的代码将要在一个进程中执行:

Message m = msgHandler.ObtainMessage();    // 注意,这里的msgHandler就是我们在上面声明的Handler,所以一定要注意其作用域。
m.Arg1 = Resource.String.string_SomeString_1;    // Arg1 和 Arg2 只能是int类型,所以一般用来传递Resource里面的东西。
m.Arg2 = Resource.String.string_SomeString_2;
m.Obj = "Message类的Obj成员,可以指定任何类型的数据,这个比较自由"。
msgHandler.SendMessage(m);

在使用Message的时候,一定要注意,Arg1Arg2只能赋值为int,所以只能使用Resource里面的物件(string、图片甚至Layout等等),而Obj就比较自由了,可以配置各种类型(因为Obj就是Object嘛~(* ̄︶ ̄))。

结束

EOF

在WPF中无操作后返回

在很多时候,我们需要使用WPF来实现一些程序,例如展示程序。最近我就做了这样的程序。但是有一个问题,就是如果做展示程序(类似触摸屏的那种),那么有必要让程序在无操作后返回到一个类似屏保的界面(可以参考windows的屏幕保护或者自动锁定)。很遗憾,我没有在wpf中发现具有这样功能的类/函数,只能转向求助于windows API。

在windows api中(“user32.dll”),有一个名为“ GetLastInputInfo”的API,在MSDN上,它的解释是这样的:

This fundtion is useful for input idle detection. However, GetLastInputInfo does not provide system-wide user input information across all running sessions. Rather, GetLastInputInfo provides session-specific user input information for only the session thar invoked the function.

The tick count when the last input event was received (see LastInputInfo) is not guaranteed to be incremental. In some cases, the value might be less than the tick count of a prior event. For example, this can be caused by a timing gap between the raw input thread and the desktop thread or an event raised by SendInput, which supplies its own tick count.

上面第一段话的意思是,GetLastInputInfo并不提供全局(在整个桌面环境中)的输入信息,而是提供特定的会话的输入信息。

第二段的意思是,接收到的输入时间的tick count并不是完全准确的,在某些情况下,可能会比实际的值要小,这种情况可能会由纯输入线程和桌面线程之间的时间差,或者是由SendInput引起。

_注:由于个人水平有限,以上引语的翻译可能不准确,所以最好阅读原文。另外,以上内容不会随着其出处更新而更新,用时请参考其出处_

所以最终我选用这个GetLastInputInfo,在C#/WPF中写好引用即可。
下面是本功能的核心代码

using System.Runtime.InteropServices; // 如果需要调用外部的dll的话,这个引用是必要的
using System.Windows;
using System.Windows.Threading;  // 此处是为了使用WPF的DispatcherTimer,
                                                                 // 如果你需要使用其他的Timer,请自行using
namespace
{
    public class TestClass
    {
        private DispatcherTimer timer; // 我们需要使用的timer,在timer里面我们要检测上一次操作的信息
        private readonly static int defultIdleAllowed = 5; // 默认的允许idle时间(单位为s),在这个时间里,如果没有操作,那么就返回,或者执行其他的操作
        private static int currentIdleTimeRemained = defaultIdleAllowed; // 当前剩余的可以idle的时间,单位为s

        public TestClass()
        {
            timer = new DispatcherTimer();
            timer.Tick += new EventHandler(Timer_Tick);
            timer.Interval = new TimeSpan(0, 0, 1);
            timer.Start();
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            if(HasActionInLastSecond())
            {
                if(--currentIdleTimeRemained == 0)
                {
                    currentIdleTime = defaultIdleAllowed;
                    timer.Stop();

                    // 在这里你可以做一些类似返回主屏幕的操作

                    timer.Start();
                }
            }
            else
            {
                currentIdleTime = defaultIdleAllowed;
            }
        }

        /// <summary>
        /// 未操作时间超过1s才开始计数
        /// </summary>
        /// <returns>bool,表示未操作时间是否超过1s</returns>
        private bool HasActionInLastSecond()
        {
            return GetIdleTime() / 1000 > 1;
        }

        /// <summary>
        /// 通过window api获得上次键鼠操作的时间,返回值为ms
        /// </summary>
        /// <param name = "pLastInputInfo">ref, 存放时间的一个结构体,在下文中会有定义</param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        static extern bool GetLastInputInfo(ref PLASTINPUTINFO pLastInputInfo);

        private static long GetIdleTime()
        {
            PLASTINPUTINFO pLastInputInfo = new PLASTINPUTINFO;
            pLastInputInfo.cbSize = Marshal.SizeOf(pLastInputInfo);
            if(!GetLastInputInfo(ref pLastInputInfo))
            {
                return 0;
            }
            return Environment.TickCount- pLastInputInfo.dwTime;
        }

        // PLASTINPUTINFO结构体定义
        [StructLayout(LayoutKind.Sequential)]
        private struct PLASTINPUTINFO
        {
            [MarshalAs(UnmanagedType.U4)]
            public int cbSize;
            [MarshalAs(UnmanagedType.U4)]
            public uint dwTime;
        }