Android客户端开发可以大致分为两种,一是纯客户端APP开发,像某宝,某东,二是与硬件进行交互的APP,比如家庭WiFi、扫码乘地铁等。我之前是做纯客户端开发,并未与硬件进行交互开发过,也是怀着诸多猜测进入目前的公司,也是仅仅用了十天左右的时间由0到1的快速开发了一款他们需要的产品…之前自己也是查阅了相关博客什么的,希望能够让自己做APP时更有底气和省时间点,但是发现找的貌似都比较深奥,不懂在说啥,所以也通过这段时间的学习整理一份简单易懂的博客,一起学习一起参考。
tip:1.本文用到了retrofit+okhttp+rxjava,不管你会不会,除了动态变化url的地方其他都无关紧要。2.本公司产品代码肯定没这么粗糙,我这边只是给需要学习的小伙伴做下实例介绍。
一、AP模式:
官方解释:AP模式其实是Access Point的简称,意思是:访问接入点。而无线网桥的AP模式,也就是利用无线网桥做无线信号的接入点了。那么,究竟是什么东西接入无线信号呢?其实就是我们平日常用到的手机、笔记本电脑、平板电脑,又或者是无线CPE(WIFI信号无线接入端),因此,我们可以理解为,AP模式,就是当做无线WIFI共享点。
简单的理解:AP模式就是一个不能上网的WiFi(用手机在WLAN中可以看到),你可以直接连接进行简单交互,比如http请求,发送协议内容。
二、业务需求
要求:手机控制机器人比如灯亮,枕头高度(机器人控制枕高),机器人给手机发送用户数据,手机端展示数据给用户看。
功能分析(该分析基于机器人程序,不同程序操作不同):
- WiFi 搜索
- wifi连接(切换)
- udp广播
- udp点对点通信
- baseUrl切换
(以下操作,根据自己的产品会把硬件叫做机器人,以下相关代码主要展示关键API,文末有GitHub连接)
1.WiFi 搜索
与硬件交互首先发现AP模式下的机器人(你可以通过手机WLAN,自主切换,但是为了减少用户的操作,我们可以这样),提供一位大牛写的工程所打包的apk,直接装上可以使用下载地址------不知道怎么免费,如果有需要留言给我,我发你邮箱。
mainWifi = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
receiverWifi = new WifiReceiver();
//注册广播
registerReceiver(receiverWifi, new IntentFilter(
WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
以注册广播的形式去接收附近的路由通道并展示到屏幕上(手机在局域网中):
点击要链接的wifi,进行网络切换
2.wifi连接(网络切换)
//链接AP路由器 AP模式下密码为空
connectWithWpa(result.SSID, "");
//切换网络总方法
private void connectWithWpa(String ssid, String pw) {
WifiUtils.withContext(getApplicationContext())
.connectWith(ssid, pw)
.setTimeout(40000)
.onConnectionResult(this::checkResult)
.start();
}
//该方法为切换wifi回调,(里面的代码下面见分晓)
private void checkResult(boolean isSuccess) {
if (isSuccess) {
if (isFirst) {
//第一次如果链接成功,向AP路由发送WiFissid以及password
showDialog();
} else {
//切换成功后,启动广播,以及接收
socket = new UDPSocket(MainActivity.this);
socket.startUDPSocket();
socket.startHeartbeatTimer("发送的广播内容,随便定义");
}
} else {
Toast.makeText(this, "fail", Toast.LENGTH_SHORT).show();
}
}
这里面网络切换会进行两次,1是连接AP模式的机器人,连接成功后,向机器人发送机器人需要的参数:
// 发送WiFi ssid以及pw以及硬件需要的数据
WifiApi.getService().lineLogin(wifiNmae, pw, "ACESPillow_neck20180906000000001").compose(MainActivity.this.<WifiBean>getDefaultTransformer())
.subscribe(new Observer<WifiBean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(WifiBean r) {
//成功返回后,进行网络切换,切换到家庭WiFi
isFirst = false;
//在这里可以用sp把之前的家庭WiFi ssid以及pw保存起来
connectWithWpa("ssid", "pw");//第二次切换网络
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
2是成功连接机器人后,切换到家庭WiFi,
//成功返回后,进行网络切换,切换到家庭WiFi
isFirst = false;
//在这里可以用sp把之前的家庭WiFi ssid以及pw保存起来
connectWithWpa("ssid", "pw");//第二次切换网络
并开始发送广播。
3.udp广播
udp通讯不需要建立通道,做简单的数据传输还是很快捷很方便的;广播分为单播、广播、多播,其中多播还是相当牛逼的,可以跨局域网,搞事情的可以研究下。
udp广播我这边封装在了UDPSocket中,主要运用了DatagramSocket ,不管是广播还是udp点对点通信用的都是它:
try {
byte[] data = new byte[BUFFER_LENGTH];
InetAddress bcIP = Inet4Address.getByAddress(Utils.getWifiBroadcastIP(context));
DatagramSocket udp = new DatagramSocket();
udp.setBroadcast(true);
DatagramPacket dp = new DatagramPacket(data, data.length, bcIP, CLIENT_PORT);
// String s = new String("0x00" + "ACESPillow_neck20180906000000001");
String s = new String("0x00" + pId);//这里的广播内容随便定义
Log.i(LOG_TAG, "Packet send from "+ dp.getAddress()+" with address: " + dp.getSocketAddress());
// 设置发送广播内容
byte[] buff =s.getBytes();
dp.setData(buff,0,buff.length);
//一次性广播,用完即关闭
udp.send(dp);
udp.disconnect();
udp.close();
} catch (IOException e) {
e.printStackTrace();
}
**dp.getAddress()**可以获取发送端的IP,这样就可以进行点对点通信,发送广播的同时,紧接着开启接收广播。
4.udp点对点通信
在这里其实就是广播接收,开启接收:
try {
Log.i(LOG_TAG, "Listener started!");
// DatagramSocket socket = new DatagramSocket(3333);
client.setSoTimeout(1500);
byte[] buffer = new byte[BUFFER_LENGTH];
DatagramPacket packet = new DatagramPacket(buffer, BUFFER_LENGTH);
while(LISTEN) {
try {
Log.i(LOG_TAG, "Listening for packets");
client.receive(packet);
//返回值
String data = new String(buffer, 0, packet.getLength());
Log.i(LOG_TAG, "Packet received from "+ packet.getAddress() +" with contents: " + data);
String socketAddress = packet.getSocketAddress() + "";
// String s = socketAddress.split(":")[0];
String address = packet.getAddress() +"";
int port = packet.getPort();
Log.i(LOG_TAG, "Packet received from "+ socketAddress+" with port: " + port);
if (port == 3333) {//判断是不是硬件方发过来的udp信息,接口筛选
// do something
//保存硬件发送过来的IP,就是address,拼接成"http:/"+ address
stopUDPSocket();//主动关闭广播
}
}
catch(IOException e) {
Log.e(LOG_TAG, "IOException in Listener " + e);
}
}
机器人在接收到广播后,会发送udp通讯,客户端只需要注意两点,1是new DatagramSocket(3333)
接口3333是之前拟定协商好的不能变,2是if (port == 3333) //判断是不是硬件方发过来的udp信息,接口筛选
排除自己发送的广播,并在其中的逻辑里关闭接收,获取packet.getAddress()
udp通讯携带的IP。因为此IP是所在局域网重新分配给机器人的新IP,手机和机器人的通讯就全靠它了。
到此,机器人由AP模式进入了局域网中
5.URl动态切换
完成1-4,当你直接与机器人进行连接时会发现404或者请求断开,查看日志你会发现,请求url还是AP模式时的Url…
网络请求框架在app初始化时就被初始化了,一个retrofit对应一个baseURl…于是乎,动态改变Url,陷入了沉思。
首先感谢:https://www.jianshu.com/p/2919bdb8d09a
大家想更清楚地了解的话先去看看他的博客吧,我只是帮忙宣扬一下我怎么用的,解释权归他
首先在Application进行对动态变化的retrofit进行初始化:
//@parm 1 需要url动态变化所对应的ke'y
//@parm2 AP_WIFI是需要动态变化的url本身,在本文中是AP模式下的URL
@Override
public void onCreate() {
super.onCreate();
//初始化要动态变化的wifi
RetrofitUrlManager.getInstance().putDomain("wifi_name", AP_WIFI);
}
构建retrofit
public WifiApi() {
OkHttpClient builder = RetrofitUrlManager.getInstance().with(new OkHttpClient.Builder()) //RetrofitUrlManager 初始化
.readTimeout(5, TimeUnit.SECONDS)
.connectTimeout(5, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) // 失败重发
// .addInterceptor(new HeaderInterceptor2())
// .addInterceptor(new LoggingInterceptor.Builder()
// .setLevel(Level.BASIC)
// .log(Platform.INFO)
// .build())
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("192.168.0.100")//这里就是上面的AP_WIFI
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 添加Rx适配器
.addConverterFactory(GsonConverterFactory.create()) // 添加Gson转换器
.client(builder)
.build();
wifiService = retrofit.create(WifiService.class);
}
在需要转换url的地方:
//连接AP模式的IP可以写入到工程里,但是硬件连接wifi成功后,会重新被当地路由重新分配一个IP,这时候就需要动态换个新生成的IP
HttpUrl httpUrl = RetrofitUrlManager.getInstance().fetchDomain("wifi_name");//application 处设置的同名
if (httpUrl == null || !httpUrl.toString().equals("192.168.0.100")) { //可以在 App 运行时随意切换某个接口的 BaseUrl
// RetrofitUrlManager.getInstance().putDomain(Constant.WIFI, Constant.WIFI_BASE_URL);
RetrofitUrlManager.getInstance().setGlobalDomain("udp收到的address");
//如果您已经确定了最终的 BaseUrl ,不需要再动态切换 BaseUrl, 请
//RetrofitUrlManager.getInstance().setRun(false);
// FIXME: 2018/12/12 再进行下面的请求
WifiApi.getService().test().compose(MainActivity.this.<WifiBean>getDefaultTransformer())
.subscribe(new Observer<WifiBean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(WifiBean r) {
//成功连接WiFi的硬件,就像个小服务器,可以根据需求进行POST 和 GET请求
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
}
});
不需要转换,就像给AP模式下的机器人发送信息时就不需要变化url,这个时候不要
HttpUrl httpUrl = RetrofitUrlManager.getInstance().fetchDomain("wifi_name");//application 处设置的同名
if (httpUrl == null || !httpUrl.toString().equals("192.168.0.100")) { /
RetrofitUrlManager.getInstance().setGlobalDomain("udp收到的address");
即可。
三、其他
<uses-permission-sdk-23 android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
所具备的权限
代码通道:
示例代码——不要吝啬你的小星星