使用Netduino和Kinect进行家庭自动化
通过.net远程控制水枪、打开车库、浇灌花园。
引言
家庭自动化一直是我长久以来的兴趣所在。市面上的技术有很多不尽如人意,而且产品价格过于昂贵,所以我决定自己动手。我最初从Arduino微控制器开始,这很有趣,但代码很快就变得难以维护,因为它不是面向对象的。此外,它也不能进行多线程或带有断点之类的真正调试。我将代码重构为C#和.NET Micro Framework。我选择了netduino plus,http://www.netduino.com/netduinoplus/specs.htm,作为微控制器,它内置了用于网络通信的以太网适配器。
请务必查看本文的后续篇章。
下图显示了该项目的早期原型。
Netduino控制的水枪
我构建的第一个项目是用于泳池的伺服控制水枪。我为netduino编写的代码控制伺服器以不同的模式喷射水枪。然后,我构建了一个Windows Phone 7界面,可以在屏幕上任意位置点击来瞄准伺服器。我使用了IIS实时流畅流式传输将视频传输到手机,这样您就可以从任何地方远程瞄准泳池里的孩子们。视频部分的结果参半,我需要在某个时候花更多时间来完善和减少缓冲时间,使其更实时。
花园
我的下一个项目是控制花园的灌溉。我的代码安排了浇灌花园的时间并控制浇灌时长。
Kinect
我在办公室的一位同事开始使用Microsoft Kinect进行项目开发,Kinect拥有丰富的SDK,包含驱动程序、API和大量优秀的示例代码。Kinect拥有一系列传感器,包括RGB摄像头、深度传感器和多阵列麦克风。有了Kinect,您就是控制器!我萌生了使用Kinect作为泳池水枪控制器的想法。现在,您可以通过指向想要射击的位置来瞄准水枪。通过弯曲另一只手臂,使手高于肘关节来控制扳机。由于丰富的Kinect API以及我已经编写了用于与netduino微控制器通信的后端层,因此连接Kinect作为控制器非常简单。
Kinect上的语音识别
Kinect的另一项功能是具有语音识别的多阵列麦克风。我尝试使用语音命令来控制水枪和打开车库。
Android车库门开启器
我想学习一些Android开发知识,所以我编写了一个原生Android应用程序来调用一个REST Web服务(WCF),该服务与netduino通信以打开车库。
整合起来
下图显示了组件之间的通信。
下图显示了Netduino控制的设备。壁炉项目正在进行中,我刚刚开始着手。
以太网通信
与netduino的以太网通信是项目中比较困难的部分。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using Microsoft.SPOT.Net.NetworkInformation;
using System.Threading;
namespace Netduino.Controller
{
public delegate void MessageEventHandler(string Message);
class EthernetCommunication
{
#region Private Variables
private string _hostAddress = null;
private int _port = 80;
private string _netduinoStaticIPAddress = null;
private string _subnetMask = null;
private string _gatewayAddress = null;
private Thread _listeningThread;
private Socket _clientSocket = null;
private static EthernetCommunication _ethernetCommunication;
#endregion
#region Constructors
//This keeps other classes from creating an instance
private EthernetCommunication()
{
}
#endregion
#region Public Properties
public string HostAddress
{
set { _hostAddress = value; }
get { return _hostAddress; }
}
public int Port
{
set { _port = value; }
get { return _port; }
}
public string NetduinoStaticIPAddress
{
set
{
_netduinoStaticIPAddress = value;
SetNetduinoStaticIPConfiguration();
}
get { return _netduinoStaticIPAddress; }
}
public string SubnetMask
{
set
{
_subnetMask = value;
SetNetduinoStaticIPConfiguration();
}
get { return _subnetMask; }
}
public string GatewayAddress
{
set
{
_gatewayAddress = value;
SetNetduinoStaticIPConfiguration();
}
get { return _gatewayAddress; }
}
#endregion
#region Events
public static event MessageEventHandler EventHandlerMessageReceived;
#endregion
#region Public Methods
private void StartListening()
{
_listeningThread = new Thread(new ThreadStart(ReceiveSocketsInListeningThreadAndHandleSocketExceptions));
_listeningThread.Start();
}
private void InitializeConfiguration()
{
if (_netduinoStaticIPAddress == null)
throw new Exception("The netduino Static IP Address nust be set!");
if (_subnetMask == null)
throw new Exception("The Subnet Mask must be set!");
if (_gatewayAddress == null)
throw new Exception("The Gateway address must be set.");
SetNetduinoStaticIPConfiguration();
NetworkInterface networkInterface = NetworkInterface.GetAllNetworkInterfaces()[0];
if (_netduinoStaticIPAddress != networkInterface.IPAddress)
throw new Exception("Problem setting the static IP.");
if (_subnetMask != networkInterface.SubnetMask)
throw new Exception("Problem setting the subnet mask.");
if (_gatewayAddress != networkInterface.GatewayAddress)
throw new Exception("Problem setting the gateway address.");
}
#endregion
#region Public Static Methods
public static EthernetCommunication GetInstance()
{
if (_ethernetCommunication == null)
{
_ethernetCommunication = new EthernetCommunication();
_ethernetCommunication.HostAddress = Config.HostAddress;
_ethernetCommunication.Port = Config.Port;
_ethernetCommunication.NetduinoStaticIPAddress = Config.NetduinoStaticIPAddress;
_ethernetCommunication.SubnetMask = Config.SubnetMask;
_ethernetCommunication.GatewayAddress = Config.GatewayAddress;
_ethernetCommunication.InitializeConfiguration();
_ethernetCommunication.StartListening();
}
return _ethernetCommunication;
}
public static void SendMessage(string message)
{
GetInstance().SendEthernetMessage(message);
}
#endregion
#region Private Methods
private bool IsSocketConnected(Socket socket)
{
bool connectionNotClosedResetOrTerminated = !socket.Poll(1000, SelectMode.SelectRead);
bool socketHasDataAvailableToRead = (socket.Available != 0);
return (connectionNotClosedResetOrTerminated || socketHasDataAvailableToRead);
}
private void ReceiveSocketsInListeningThreadAndHandleSocketExceptions()
{
try
{
ReceiveSocketsInListeningThread();
}
catch (SocketException se)
{
Debug.Print("Socket Exception! Probably WiFi or Ethernet connection not working?");
Debug.Print(se.StackTrace);
Debug.Print("Rebooting netduino to recover.");
PowerState.RebootDevice(false);
}
catch (Exception ex)
{
Debug.Print("Non socket exception.");
Debug.Print(ex.StackTrace);
}
}
private void ReceiveSocketsInListeningThread()
{
string receiveMessage = "";
bool exitProgram = false;
using (System.Net.Sockets.Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
socket.Bind(new IPEndPoint(IPAddress.Any, _port));
socket.Listen(10);
while (!exitProgram)
{
Debug.Print("Waiting for message from socket...");
// This call is "blocking" and will will wait for a socket.
// The thread will wait here until a message is received
_clientSocket = socket.Accept();
Debug.Print("Message received!");
using (_clientSocket)
{
while (IsSocketConnected(_clientSocket))
{
int availablebytes = _clientSocket.Available;
byte[] buffer = new byte[availablebytes];
_clientSocket.Receive(buffer);
if (buffer.Length > 0)
{
receiveMessage = new string(Encoding.UTF8.GetChars(buffer));
RaiseMessageReceivedEvent(receiveMessage);
if (receiveMessage.ToUpper() == "EXIT")
{
exitProgram = true;
}
}
}
}
}
}
}
private void RaiseMessageReceivedEvent(string message)
{
// Event will be null if there are no subscribers
if (EventHandlerMessageReceived != null)
{
EventHandlerMessageReceived(message);
}
}
private void SetNetduinoStaticIPConfiguration()
{
//Exit if not all of the configuration properties are set
if (_netduinoStaticIPAddress == null || _subnetMask == null || _gatewayAddress == null)
return;
NetworkInterface networkInterface = NetworkInterface.GetAllNetworkInterfaces()[0];
bool _ipAddressAlreadySet = _netduinoStaticIPAddress == networkInterface.IPAddress;
bool _subnetMaskAlreadySet = _subnetMask == networkInterface.SubnetMask;
bool _gatewayAlreadySet = _gatewayAddress == networkInterface.GatewayAddress;
if (_ipAddressAlreadySet && _subnetMaskAlreadySet && _gatewayAlreadySet)
return;
// Set our IP address to a new value
// This will be saved in the config sector of the netduino and will survive reboots
networkInterface.EnableStaticIP(_netduinoStaticIPAddress, _subnetMask, _gatewayAddress);
}
private void SendEthernetMessage(string message)
{
if (_hostAddress != null && _port > 0)
{
using (System.Net.Sockets.Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IPHostEntry entry = Dns.GetHostEntry(_hostAddress);
IPAddress address = entry.AddressList[0];
IPEndPoint endpoint = new IPEndPoint(address, _port);
try
{
socket.Connect(endpoint);
socket.Send(Encoding.UTF8.GetBytes(message));
socket.Close();
Debug.Print(message);
}
catch (SocketException se)
{
Debug.Print("Socket Exception! Probably no server or bad ip?");
Debug.Print(se.StackTrace);
Debug.Print("Rebooting netduino to recover.");
PowerState.RebootDevice(false);
}
catch (Exception ex)
{
Debug.Print("Non socket exception.");
Debug.Print(ex.StackTrace);
}
}
}
}
#endregion
}
}
如果您想查看桌面应用程序通信的示例,请下载源代码并查看Netduino.Desktop.Messenger项目。
伺服器通信
编程伺服器很有趣。我编写了一个伺服器类,您可以设置角度以及最小和最大度数。我添加了Inverted属性来反转角度,因为我有一个室内版本的水枪,它从地板上安装,而室外版本是倒置安装的。
using System;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
namespace Netduino.Controller
{
public class Servo : IDisposable
{
#region Private Variables
private PWM _servo;
private bool _invertAngle = false;
private int _degreeMin = Config.DegreeMinDefault;
private int _degreeMax = Config.DegreeMaxDefault;
private uint _durationMin = Config.DurationMinDefault;
private uint _durationMax = Config.DurationMaxDefault;
private uint _angle = Config.HomeDefaultAngle;
private uint _period = Config.PeriodDefault;
#endregion
#region Constructors
public Servo(Cpu.Pin pin)
{
_servo = new PWM(pin);
_servo.SetDutyCycle(0);
}
#endregion
#region Public Methods
public void Dispose()
{
DisengageServo();
_servo.Dispose();
}
/// <summary>
/// Disengage the servo.
/// The servo motor will stop trying to maintain an angle
///
public void DisengageServo()
{
_servo.SetDutyCycle(0);
}
public void EngageServo()
{
SetPulse();
}
#endregion
#region Private Methods
private void SetPulse()
{
uint angle = _invertAngle ? 180 - _angle: _angle;
uint duration = (angle) * (_durationMax - _durationMin) / 180 + _durationMin;
_servo.SetPulse(period: _period, duration: duration);
}
#endregion
#region Public Properties
public int Angle
{
set
{
if (value > _degreeMax)
value = _degreeMax;
if (value < _degreeMin)
value = _degreeMin;
if (value < 0)
value = 0;
_angle = (uint)value;
SetPulse();
}
get
{
return (int)_angle;
}
}
public bool InvertAngle
{
set {_invertAngle = value;}
get { return _invertAngle; }
}
public int DegreeMin
{
set {_degreeMin = value;}
get { return _degreeMin; }
}
public int DegreeMax
{
set { _degreeMax = value; }
get { return _degreeMax; }
}
public uint durationMin
{
set { _durationMin = value; }
get { return _durationMin; }
}
public uint durationMax
{
set { _durationMax = value; }
get { return _durationMax; }
}
public uint period
{
set { _period = value; }
get { return _period; }
}
#endregion
}
}
控制花园
我为.NET Micro Framework编写了一个时间命令库。请注意,.net micro framework功能非常丰富,但不支持泛型。
using System;
using System.Threading;
using System.Collections;
using Microsoft.SPOT;
namespace Netduino.Controller
{
public delegate void AlarmCallback();
class AlarmData
{
public int Key { get; set; }
public ExtendedTimer ExtendedTimer { get; set; }
public bool RemoveAfterRun { get; set; }
public AlarmCallback Callback { get; set; }
}
class Time
{
#region Private Variables
private static Hashtable _alarmHashtable;
private static int _key;
#endregion
#region Constructors
//This keeps other classes from creating an instance
private Time()
{
}
#endregion
#region Public Static Methods
public static void SetTime(int year, int month, int day, int hour,int minute, int second, int millisecond )
{
DateTime presentTime = new DateTime( year, month, day, hour, minute, second, millisecond);
Microsoft.SPOT.Hardware.Utility.SetLocalTime(presentTime);
}
public static void RunDaily(AlarmCallback alarmCallback, int hour, int minute, int second)
{
DateTime alarmTime = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, hour, minute, second, 0);
//If we already missed today then tomorrow is the first day to run
if(alarmTime<DateTime.Now)
{
alarmTime = alarmTime.AddDays(1);
}
TimeSpan dailyTimeSpan = new TimeSpan(24, 0, 0);
CreateAlarm(alarmCallback, alarmTime, dailyTimeSpan, false);
}
public static void RunOnDelay(AlarmCallback alarmCallback, int runInMilliseconds)
{
DateTime alarmTime = DateTime.Now.AddMilliseconds(runInMilliseconds);
CreateAlarm(alarmCallback, alarmTime, TimeSpan.Zero, true);
}
public static void RunRepetitively(AlarmCallback alarmCallback, int repeatMilliseconds)
{
DateTime alarmTime = DateTime.Now.AddMilliseconds(repeatMilliseconds);
TimeSpan repeatTimeSpan = new TimeSpan(0, 0, 0, 0, repeatMilliseconds);
CreateAlarm(alarmCallback, alarmTime, repeatTimeSpan, false);
}
#endregion
#region Private Methods
private static void CreateAlarm(AlarmCallback alarmCallback, DateTime alarmTime, TimeSpan timeSpan, bool removeAfterRun)
{
if (_alarmHashtable == null)
_alarmHashtable = new Hashtable();
_key=_key+1;
AlarmData alarmData = new AlarmData();
alarmData.Key = _key;
alarmData.Callback = alarmCallback;
alarmData.ExtendedTimer = new ExtendedTimer(OnExecuteAlarm, alarmData, alarmTime, timeSpan);
alarmData.RemoveAfterRun = removeAfterRun;
_alarmHashtable.Add(_key, alarmData);
}
private static void OnExecuteAlarm(object target)
{
AlarmData alarmData = (AlarmData)target;
if (alarmData.RemoveAfterRun)
_alarmHashtable.Remove(alarmData.Key);
alarmData.Callback.Invoke();
}
#endregion
}
}
主.NET Micro Framework程序
当收到消息时,有一个处理以太网通信的事件处理程序会运行。
EthernetCommunication.EventHandlerMessageReceived += new MessageEventHandler(OnMessageReceived);
OnMessageReceived方法解析消息并调用相应的方法来执行命令。下面的代码片段仅为部分内容,完整的源代码可在本文中下载。
private static void OnMessageReceived(string message)
{
string[] parts = message.Split(' ');
switch(parts[0].ToUpper())
{
case "M":
case "MOVE":
if (parts.Length != 3)
{
EthernetCommunication.SendMessage("The move command takes 3 arguments.");
break;
}
int leftRightAngle = int.Parse(parts[1]);
int upDownAngle = int.Parse(parts[2]);
_squirtGun.MoveToPosition(leftRightAngle, upDownAngle);
break;
case "U":
case "UP":
int upDelta = parts.Length > 1 ? int.Parse(parts[1]) : 1;
_squirtGun.UpDownAngle = _squirtGun.UpDownAngle + upDelta;
break;
关于作者
在Twitter上关注Dan:@LogicalDan
Dan以优异的成绩毕业于北卡罗来纳州立大学,获得电气工程和计算机工程双学位。Dan曾获得通用汽车的全额奖学金进入北卡罗来纳州立大学。在通用汽车工作后,Dan曾担任卡罗莱纳地区最大的微软商业解决方案合作伙伴的应用开发总监。在此期间,Dan的团队赢得了两个微软 Pinnacle奖。在过去的12年里,作为Logical Advantage(www.logicaladvantage.com)的联合创始人兼首席技术官,Dan已成功为众多财富500强公司设计并交付了基于Web和移动端的应用程序。Dan专注于新兴技术,并确保所有项目都能够满足客户当前和未来的需求。Dan与他的首席解决方案官和其他架构师合作,创建技术标准,包括编码标准、工具和平台。他在当地的Microsoft Enterprise Developer's Guild担任领导职务,并已在该指导委员会任职十多年。
下载源代码
点击此处下载netduino .net micro framework项目的源代码。
继续阅读我的家庭自动化系列。
点击此链接阅读我的家庭自动化项目的第二部分:使用jQuery Mobile、MVC和Netduino进行家庭自动化