树莓派Wifi小车(二)


上篇文章 之后,继续折腾我的小车,又用C#和Android 写了两个控制客户端。

思路比较直接,使用POST请求去发送小车转向参数,然后就动起来了。C#不说了,很熟练写起来也非常简单,使用钩子捕获全局键盘,这样及时程序不在前台也能控制小车。

核心代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace WangHeng.Org.PiCar
{
public partial class MainFrm : Form
{
public MainFrm()
{
InitializeComponent();
InitControl();
KeyHookUtils.Hook_Start();
}

private void InitControl()
{
this.StartPosition = FormStartPosition.CenterScreen;
this.Text = "PiCar Control v" + this.ProductVersion;
this.MaximizeBox = false;
this.MinimizeBox = true;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;

this.btnDown.Tag = "t_down";
this.btnLeft.Tag = "t_left";
this.btnRight.Tag = "t_right";
this.btnUp.Tag = "t_up";
this.btnStop.Tag = "t_stop";
this.statusStrip1.SizingGrip = false;
this.lblPostResultInfo.Text = "启动成功!";
}
protected override bool ProcessDialogKey(Keys keyData)
{
if (keyData == Keys.Left)
{
lblPostResultInfo.Text = "left";
}
return base.ProcessDialogKey(keyData);
}

private void ctlButton_Click(object sender, EventArgs e)
{
var btn = sender as Button;
var result = CarCtl.TurnFunc(btn.Tag.ToString());
this.lblPostResultInfo.Text = "请求成功 - " + result;
}


}

public static class CarCtl
{
public enum CarStatus
{
Left,
Right,
Up,
Down,
Stop,
Unknown
}

public static CarStatus CurrentStatus = CarStatus.Unknown;
public static string TurnFunc(string direcID)
{
try
{
var requestUrl = "http://192.168.1.171:2000/ctl";
var requestData = new Dictionary<string, string>() { { "id", direcID } };
var requestUserAgent = "WangHeng PiCar/v1.0";
var requestEncoding = Encoding.Default;
var result = HttpUtility.CreatePostHttpResponse(
requestUrl,
requestData,
5000,
requestUserAgent,
requestEncoding,
null);

using (var sr = new StreamReader(result.GetResponseStream(), requestEncoding))
{
return sr.ReadToEnd();
}
}
catch (Exception ex)
{
return null;
}
}
}


public class KeyHookUtils
{
[StructLayout(LayoutKind.Sequential)]
public class KeyBoardHookStruct
{
public int vkCode;
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
}
//委托
public delegate int HookProc(int nCode, int wParam, IntPtr lParam);
static int hHook = 0;
public const int WH_KEYBOARD_LL = 13;
//LowLevel键盘截获,如果是WH_KEYBOARD=2,并不能对系统键盘截取,Acrobat Reader会在你截取之前获得键盘。
static HookProc KeyBoardHookProcedure;

//设置钩子
[DllImport("user32.dll")]
public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

//抽掉钩子
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern bool UnhookWindowsHookEx(int idHook);

//调用下一个钩子
[DllImport("user32.dll")]
public static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);

[DllImport("kernel32.dll")]
public static extern int GetCurrentThreadId();

[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string name);

public static void Hook_Start()
{
if (hHook == 0)
{
KeyBoardHookProcedure = new HookProc(KeyBoardHookProc);
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyBoardHookProcedure,
GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0);
//如果设置钩子失败.
if (hHook == 0)
{
Hook_Clear();
}
}
}

/// <summary>
/// 取消钩子事件
/// </summary>
public static void Hook_Clear()
{
bool retKeyboard = true;
if (hHook != 0)
{
retKeyboard = UnhookWindowsHookEx(hHook);
hHook = 0;
}
}

public static int KeyBoardHookProc(int nCode, int wParam, IntPtr lParam)
{
if (nCode >= 0)
{
KeyBoardHookStruct kbh = (KeyBoardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyBoardHookStruct));
Keys k = (Keys)Enum.Parse(typeof(Keys), kbh.vkCode.ToString());
switch (k)
{
case Keys.Left:
if (kbh.flags == 1)
{
// 这里写按下后做什么事
if (CarCtl.CurrentStatus != CarCtl.CarStatus.Up)
{
CarCtl.TurnFunc("t_left");
CarCtl.CurrentStatus = CarCtl.CarStatus.Up;
}
}
else if (kbh.flags == 129)
{
//放开后做什么事
CarCtl.TurnFunc("t_stop");
CarCtl.CurrentStatus = CarCtl.CarStatus.Stop;
}
return 1;
case Keys.Up:
if (kbh.flags == 1)
{
// 这里写按下后做什么事
if (CarCtl.CurrentStatus != CarCtl.CarStatus.Up)
{
CarCtl.TurnFunc("t_up");
CarCtl.CurrentStatus = CarCtl.CarStatus.Up;
}
}
else if (kbh.flags == 129)
{
//放开后做什么事
CarCtl.TurnFunc("t_stop");
CarCtl.CurrentStatus = CarCtl.CarStatus.Stop;
}
return 1;
case Keys.Right:
if (kbh.flags == 1)
{
if (CarCtl.CurrentStatus != CarCtl.CarStatus.Up)
{
CarCtl.TurnFunc("t_right");
CarCtl.CurrentStatus = CarCtl.CarStatus.Up;
}
}
else if (kbh.flags == 129)
{
//放开后做什么事
CarCtl.TurnFunc("t_stop");
CarCtl.CurrentStatus = CarCtl.CarStatus.Stop;
}
return 1;
case Keys.Down:
if (kbh.flags == 1)
{
if (CarCtl.CurrentStatus != CarCtl.CarStatus.Up)
{
CarCtl.TurnFunc("t_down");
CarCtl.CurrentStatus = CarCtl.CarStatus.Up;
}
}
else if (kbh.flags == 129)
{
//放开后做什么事
CarCtl.TurnFunc("t_stop");
CarCtl.CurrentStatus = CarCtl.CarStatus.Stop;
}
return 1;
default:
break;
}
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
}
}


这里主要说一下Android 的客户端

Android之前虽没怎么接触过,但凭着自己一些基础,还是慢慢摸索到了门路。唯一比较坑的是,在获取Wifi权限的时候,照着网上的介绍改了AndroidManifest.xml 加入
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

依然没有访问网络的权限 :(

为此还特意加了几个Android的开发群,但是群里一个个爱答不理,趾高气扬的态度,我就呵呵了。果断退群,还是得靠自己,扶梯子上Google,一顿搜搜搜,果然有同病相怜的朋友。

原因竟然是一个小问题目标SDK版本不对。。。好吧,果然弱爆了。

android_target_sdk_version_error

 

 

 

按上图将Target SDK Version 从18改成19,运行,测试,这次果然可以了。如下:

piCar截图

 

 

 

 

 

 

 

 

 

 

代码也比较简单,核心代码如下:
package com.apiof.picar;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.StrictMode;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.view.View.OnClickListener;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 禁止屏幕锁屏,也可以用View.setKeepScreenOn(boolean)来实现
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
InitHandler();

TestNetWork();
}

public void DisplayToast(String str)
{
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

public boolean onKeyDown(int KeyCode, KeyEvent event) {
switch (KeyCode) {
case KeyEvent.KEYCODE_0:
DisplayToast("按下数字0");
break;
}
return super.onKeyDown(KeyCode, event);
}

@Override
public boolean onKeyUp(int keyCode,KeyEvent event){
switch(keyCode){
case KeyEvent.KEYCODE_0:
DisplayToast("松开数字0");
break;
}
return super.onKeyUp(keyCode, event);
}

public void TestNetWork() {

String strUrl = "http://nas.apiof.com:2000";
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads().detectDiskWrites().detectNetwork()
.penaltyLog().build());
URL url = null;
try {
url = new URL(strUrl);
System.out.println(url.getPort());
HttpURLConnection urlConn = (HttpURLConnection) url
.openConnection();
InputStreamReader in = new InputStreamReader(
urlConn.getInputStream());
BufferedReader br = new BufferedReader(in);
String result = "";
String readerLine = null;
while ((readerLine = br.readLine()) != null) {
result += readerLine;
}
in.close();
urlConn.disconnect();

System.out.println("r:" + result);
TextView textView = (TextView) this.findViewById(R.id.textView2);
textView.setText(result);
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

@Override
public void onBackPressed() {
new AlertDialog.Builder(this).setTitle("确认退出吗?")
.setIcon(android.R.drawable.ic_dialog_info)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {

@Override
public void onClick(DialogInterface dialog, int which) {
// 点击“确认”后的操作
MainActivity.this.finish();
}
})
.setNegativeButton("返回", new DialogInterface.OnClickListener() {

@Override
public void onClick(DialogInterface dialog, int which) {
// 点击“返回”后的操作,这里不设置没有任何操作
}
}).show();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

private String intToIp(int i) {

return (i & 0xFF) + "." + ((i >> 8) & 0xFF) + "." + ((i >> 16) & 0xFF)
+ "." + (i >> 24 & 0xFF);
}

public String getLocalIpAddress() {
try {
for (Enumeration<NetworkInterface> en = NetworkInterface
.getNetworkInterfaces(); en.hasMoreElements();) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf
.getInetAddresses(); enumIpAddr.hasMoreElements();) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()) {
return inetAddress.getHostAddress().toString();
}
}
}
} catch (SocketException ex) {
Log.e("WifiPreference IpAddress", ex.toString());
}
return null;
}

public void InitHandler() {
// 获取wifi服务
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
// 判断wifi是否开启
if (!wifiManager.isWifiEnabled()) {
wifiManager.setWifiEnabled(true);
}
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int ipAddress = wifiInfo.getIpAddress();
String ip = intToIp(ipAddress);
TextView txt1 = (TextView) findViewById(R.id.textView1);
txt1.setText(txt1.getText() + "\r\nLocal IP: " + ip);


((Button)findViewById(R.id.btnStop)).setOnClickListener(stopHandler);
((Button)findViewById(R.id.btnForward)).setOnClickListener(upHandler);
((Button)findViewById(R.id.btnBackward)).setOnClickListener(downHandler);
((Button)findViewById(R.id.btnTurnLeft)).setOnClickListener(leftHandler);
((Button)findViewById(R.id.btnTurnRight)).setOnClickListener(rightHandler);
}

private void Trun(String id) {
TextView txt = (TextView) findViewById(R.id.textView2);
try {
String requestUrl = "http://192.168.1.171:2000/ctl";
Map<String, String> requestParams = new HashMap<String, String>();
requestParams.put("id", id);
String result = HttpUtils.submitPostData(requestUrl,
requestParams, "utf-8");

txt.setText("turn to: "+result);
} catch (Throwable e) {
e.printStackTrace();
}
}

private OnClickListener stopHandler=new OnClickListener() {
public void onClick(View v) {
Trun("t_stop");
}
};
private OnClickListener leftHandler=new OnClickListener() {
public void onClick(View v) {
Trun("t_left");
}
};
private OnClickListener rightHandler=new OnClickListener() {
public void onClick(View v) {
Trun("t_right");
}
};
private OnClickListener upHandler=new OnClickListener() {
public void onClick(View v) {
Trun("t_up");
}
};
private OnClickListener downHandler=new OnClickListener() {
public void onClick(View v) {
Trun("t_down");
}
};

}

完整的代码放到了我的Github 共享,需要的朋友可以参考下载:

https://github.com/wujiwh/piCar/tree/master/client/android
  • 本文作者: 王恒
  • 本文链接: raspberry-wifi-car-2.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!