在本文中,将使用UDP广播的方式实现APP远程控制智能插座。
基本原理
Android 手机和智能插座连接到家庭路由器中。APP通过UDP广播的方式向ESP8266发送控制命令。ESP8266接收到控制命令后,执行相应的操作并返回结果。
通信帧格式
采用UDP的方式,不需要考虑TCP中存在的粘包的问题。为了简单起见,采用JSON格式传输数据。
控制命令ID:
const int MESSAGE_GETSWITCHSTATE_REQUEST = 1000;
const int MESSAGE_GETSWITCHSTATE_RESPONSE = 1001;
const int MESSAGE_SETSWITCHSTATE_REQUEST = 1002;
const int MESSAGE_SETSWITCHSTATE_RESPONSE = 1003;
返回结果:
const int STATUS_OK = 200;
const int STATUS_ERR = 304;
通信过程示例:
获取开关状态:
//APP:
{"Cmd":1000}
//ESP8266:
{"Cmd":1001,"Status":200,"SwitchState":false}
控制开关状态:
//APP:
{"Cmd":1002,"SwitchState":true}
//ESP8266:
{"Cmd":1003,"Status":200}
实现过程
源代码目前已开源:https://git.coding.net/zimengyu1992/WIFISocketProject.git
设备端
1)采用WiFiUdp类创建了UDP服务器。
2)采用ArduinoJson库来解析JSON命令包。
核心源码如下:
#ifndef __WIFISOCKETUDPSERVER_H__
#define __WIFISOCKETUDPSERVER_H__
#include <Arduino.h>
#include <WiFiUdp.h>
#include <ESP8266WiFi.h>
class WIFISocketUdpServerClass
{
private:
const int UDP_SERVER_PORT = 10000;
const int STATUS_OK = 200;
const int STATUS_ERR = 304;
const int MESSAGE_GETSWITCHSTATE_REQUEST = 1000;
const int MESSAGE_GETSWITCHSTATE_RESPONSE = 1001;
const int MESSAGE_SETSWITCHSTATE_REQUEST = 1002;
const int MESSAGE_SETSWITCHSTATE_RESPONSE = 1003;
WiFiUDP mUdp;
public:
void begin();
void loop();
};
extern WIFISocketUdpServerClass WIFISocketUdpServer;
#endif
#include <ArduinoJson.h>
#include "WIFISocketSwitch.h"
#include "WIFISocketUdpServer.h"
WIFISocketUdpServerClass WIFISocketUdpServer;
void WIFISocketUdpServerClass::begin()
{
mUdp.begin(UDP_SERVER_PORT);
}
void WIFISocketUdpServerClass::loop()
{
const int PACKET_MAXSIZE = 128;
uint8_t packet[PACKET_MAXSIZE];
memset(packet, 0, sizeof(char)*PACKET_MAXSIZE);
if (!mUdp.parsePacket())
{
return;
}
mUdp.read(packet, PACKET_MAXSIZE);
Serial.println((const char *)packet);
DynamicJsonBuffer jsonBuffer(PACKET_MAXSIZE);
JsonObject& root = jsonBuffer.parseObject(packet);
if (!root.success())
{
return;
}
if (!root.containsKey("Cmd"))
{
return;
}
int cmd = (int)root["Cmd"];
if (cmd == MESSAGE_GETSWITCHSTATE_REQUEST)
{
jsonBuffer.clear();
JsonObject& Root = jsonBuffer.createObject();
Root["Cmd"] = MESSAGE_GETSWITCHSTATE_RESPONSE;
Root["Status"] = STATUS_OK;
Root["SwitchState"] = WIFISocketSwitch.getState();
memset(packet, 0, PACKET_MAXSIZE);
size_t length = Root.printTo((char *)packet, PACKET_MAXSIZE);
mUdp.beginPacket(mUdp.remoteIP(), mUdp.remotePort());
mUdp.write(packet, length);
mUdp.endPacket();
}
else if (cmd == MESSAGE_SETSWITCHSTATE_REQUEST)
{
boolean Successed = false;
if (root.containsKey("SwitchState"))
{
boolean SwitchState = (boolean)root["SwitchState"];
WIFISocketSwitch.Switch(SwitchState);
Successed = true;
}
jsonBuffer.clear();
JsonObject& Root = jsonBuffer.createObject();
Root["Cmd"] = MESSAGE_SETSWITCHSTATE_RESPONSE;
Root["Status"] = Successed ? STATUS_OK : STATUS_ERR;
memset(packet, 0, PACKET_MAXSIZE);
size_t length = Root.printTo((char *)packet, PACKET_MAXSIZE);
mUdp.beginPacket(mUdp.remoteIP(), mUdp.remotePort());
mUdp.write(packet, length);
mUdp.endPacket();
}
}
APP端源码
1)获取手机连接WIFI的状态以及本机的IP地址;通过本机IP推断广播IP。
如本机IP为192.168.1.120,则广播地址为192.168.1.255
2)采用DatagramSocket类发送&接收数据包;为了不阻塞UI线程,创建了UDP收发的独立线程。
3)采用Message&Handler的方式,进行UI线程通信;
核心源码如下:
package site.webhome.wifisocketliteapp;
public class StatusCode {
public static final int MESSAGE_ARRIVE = 10000;
public static final int MESSAGE_TIMEOUT = 10001;
public static final int STATUS_OK = 200;
public static final int STATUS_ERR = 304;
public static final int MESSAGE_GETSWITCHSTATE_REQUEST = 1000;
public static final int MESSAGE_GETSWITCHSTATE_RESPONSE = 1001;
public static final int MESSAGE_SETSWITCHSTATE_REQUEST = 1002;
public static final int MESSAGE_SETSWITCHSTATE_RESPONSE = 1003;
}
package site.webhome.wifisocketliteapp;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
public class WiFiUtility {
public static String getIPAddress(Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
//检查Wifi状态
if (!wm.isWifiEnabled()) {
return null;
}
WifiInfo wi = wm.getConnectionInfo();
//获取32位整型IP地址
int Ip = wi.getIpAddress();
//把整型地址转换成“*.*.*.*”地址
return IpToString(Ip);
}
public static String getBroadCastIPAddress(Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
if (!wm.isWifiEnabled()) {
return null;
}
WifiInfo wi = wm.getConnectionInfo();
int Ip = wi.getIpAddress();
Ip = Ip | (0xFF << 24);
return IpToString(Ip);
}
private static String IpToString(int Ip) {
return (Ip & 0xFF) + "." +
((Ip >> 8) & 0xFF) + "." +
((Ip >> 16) & 0xFF) + "." +
(Ip >> 24 & 0xFF);
}
}
package site.webhome.wifisocketliteapp;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.security.MessageDigest;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
public class UdpConnection extends Thread {
private final static String mTAG = UdpConnection.class.getSimpleName();
private Handler mHandler = null;
private String mDestIP = null;
private int mDestPort = 10000;
private DatagramSocket mSocket = null;
private Semaphore mSemaphore = null;
private BlockingQueue<String> mQueue = null;
public UdpConnection(Handler handler, String destIP, int destPort) throws SocketException {
mHandler = handler;
mDestIP = destIP;
mDestPort = destPort;
mQueue = new LinkedBlockingQueue<String>();
mSemaphore = new Semaphore(0);
mSocket = new DatagramSocket();
mSocket.setSoTimeout(3000);
this.start();
}
public void run() {
while (true) {
try {
mSemaphore.acquire();
if (!mQueue.isEmpty()) {
String sendMessage = mQueue.poll();
udpSend(sendMessage);
String receiveMessage = udpReceive();
if (mHandler != null) {
Message responseMessage = new Message();
responseMessage.what = StatusCode.MESSAGE_ARRIVE;
Bundle data = new Bundle();
data.putString("MESSAGE", receiveMessage);
responseMessage.setData(data);
mHandler.sendMessage(responseMessage);
}
}
} catch (Exception e) {
e.printStackTrace();
Message responseMessage = new Message();
responseMessage.what = StatusCode.MESSAGE_TIMEOUT;
mHandler.sendMessage(responseMessage);
}
}
}
private void udpSend(String Message) throws IOException {
InetAddress IPAddress = InetAddress.getByName(mDestIP);
byte[] sendBuffer = Message.getBytes();
DatagramPacket packetSend = new DatagramPacket(sendBuffer, sendBuffer.length, IPAddress, mDestPort);
mSocket.send(packetSend);
}
private String udpReceive() throws IOException {
final int PACKET_MAXSIZE = 128;
DatagramPacket packetReceive = new DatagramPacket(new byte[PACKET_MAXSIZE], PACKET_MAXSIZE);
mSocket.receive(packetReceive);
String Message = new String(packetReceive.getData(), 0, packetReceive.getLength());
return Message;
}
public void send(String Message) throws InterruptedException {
mQueue.put(Message);
mSemaphore.release();
}
}
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.JsonReader;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
public class MainActivity extends AppCompatActivity {
private Button switchButton = null;
private boolean switchState = false;
private boolean setSwitchState = false;
private boolean switchStateInited = false;
private boolean requesting = false;
private UdpConnection udpConnection = null;
public Handler MainHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == StatusCode.MESSAGE_ARRIVE) {
try {
String MessageText = msg.getData().getString("MESSAGE");
JSONObject Json = new JSONObject(MessageText);
int Status = Json.getInt("Status");
if (Status == StatusCode.STATUS_ERR) {
Toast.makeText(getApplicationContext(), "参数错误!", Toast.LENGTH_SHORT).show();
} else {
int Cmd = Json.getInt("Cmd");
if (Cmd == StatusCode.MESSAGE_GETSWITCHSTATE_RESPONSE) {
switchStateInited = true;
switchState = Json.getBoolean("SwitchState");
ChangeBackground(switchState);
}
if (Cmd == StatusCode.MESSAGE_SETSWITCHSTATE_RESPONSE) {
switchState = setSwitchState;
ChangeBackground(switchState);
Toast.makeText(getApplicationContext(), "操作成功!", Toast.LENGTH_SHORT).show();
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (msg.what == StatusCode.MESSAGE_TIMEOUT) {
Toast.makeText(getApplicationContext(), "网络超时!", Toast.LENGTH_SHORT).show();
}
requesting = false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
switchButton = (Button) findViewById(R.id.switch_button_device);
switchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SwitchButtonOnClick();
}
});
ChangeBackground(switchState);
InitStatus();
}
private boolean CheckInWifiState() {
return (WiFiUtility.getIPAddress(this) != null);
}
private void InitStatus() {
if (!CheckInWifiState()) {
Toast.makeText(getApplicationContext(), "请先连接WIFI!", Toast.LENGTH_SHORT).show();
} else {
try {
if (udpConnection == null) {
udpConnection = new UdpConnection(MainHandler, WiFiUtility.getBroadCastIPAddress(this), 10000);
}
SendGetSwitchState();
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "网络超时!", Toast.LENGTH_SHORT).show();
}
}
}
private void SwitchButtonOnClick() {
if (requesting) {
Toast.makeText(getApplicationContext(), "操作未完成!", Toast.LENGTH_SHORT).show();
return;
}
if (!CheckInWifiState()) {
Toast.makeText(getApplicationContext(), "请先连接WIFI!", Toast.LENGTH_SHORT).show();
switchStateInited = false;
if (udpConnection != null) {
if (udpConnection.isAlive()) {
udpConnection.interrupt();
}
udpConnection = null;
}
return;
}
if (!switchStateInited) {
Toast.makeText(getApplicationContext(), "正在初始化!", Toast.LENGTH_SHORT).show();
InitStatus();
return;
}
try {
setSwitchState = !switchState;
SendSetSwitchState(setSwitchState);
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "网络超时!", Toast.LENGTH_SHORT).show();
}
}
private void SendGetSwitchState() throws JSONException, InterruptedException {
JSONObject json = new JSONObject();
json.put("Cmd", StatusCode.MESSAGE_GETSWITCHSTATE_REQUEST);
udpConnection.send(json.toString());
}
private void SendSetSwitchState(boolean state) throws JSONException, InterruptedException {
JSONObject json = new JSONObject();
json.put("Cmd", StatusCode.MESSAGE_SETSWITCHSTATE_REQUEST);
json.put("SwitchState", state);
udpConnection.send(json.toString());
}
private void ChangeBackground(boolean switchState) {
int statusBarRes = R.color.colorBluePrimaryDark;
int backgroundRes = R.drawable.switch_on_bg;
if (!switchState) {
backgroundRes = R.drawable.switch_off_bg;
statusBarRes = R.color.colorBlackPrimaryDark;
}
RelativeLayout relativeLayout = (RelativeLayout) findViewById(R.id.layout_main_device);
relativeLayout.setBackgroundResource(backgroundRes);
Window window = this.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(this.getResources().getColor(statusBarRes));
}
}