1. 程式人生 > >Android 與AP模式下的硬體連線(嵌入式基礎需求)

Android 與AP模式下的硬體連線(嵌入式基礎需求)

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"/>

所具備的許可權
程式碼通道:
示例程式碼——不要吝嗇你的小星星