StarRTC , AndroidThings , 樹莓派小車,公網環境,視頻遙控(二)小車端


原文地址:http://blog.starrtc.com/?p=94

 

1 創建工程
IDE:Android Studio 3.1;
File>New>New Project>輸入項目名>next>Target Android Devices 復選框勾選 Phone and Tablet 和 Android Things>next… finish;
創建成功后項目會包括mobile和things兩個module,mobile是手機端程序,things是小車上的程序。
things部分編譯出來的是apk只能運行在Android Things系統上,后面我們會在樹莓派上運行這module。

2 導入StarRTC SDK
SDK下載:https://docs.starrtc.com/download/
開發文檔:https://docs.starrtc.com/docs/android-1.html
按照開發文檔所述,分別在mobile和things兩個module下導入StarRTC的sdk。

3 開始碼代碼,小車端程序(things module)
這個項目的代碼大部分都是從StarRTC官網demo源碼中拷貝過來的,並做了些簡單的修改。
3.1 小車開機后原地待命
小車要先登錄StarRTC的服務,后邊才能接收到手機端的啟動指令。

//設置小車的ID,后邊要通過這個ID來喚醒小車
MLOC.userId = "car0001";
//設置適合樹莓派的默認參數值
XHClient.getInstance().setDefaultConfig(true,true,0,0,1,false,false,false,false,XHConstants.XHCropTypeEnum.STAR_VIDEO_CROP_CONFIG_BIG_NOCROP_SMALL_NONE);
XHClient.getInstance().setCustomEncoderConfig(640,480,640,480,15,500,45);
//初始化SDK,添加登錄和消息狀態監聽
XHClient.getInstance().initSDK(this, new XHSDKConfig(MLOC.agentId),MLOC.userId);
XHClient.getInstance().getChatManager().addListener(new XHChatManagerListener());
XHClient.getInstance().getLoginManager().addListener(new XHLoginManagerListener());
checkNetAvailable();

這里有個問題要注意一下,本來這段初始化代碼最后,就要進行登錄操作了,但樹莓派開機時,會第一時間自動運行小車的程序,這樣就可能遇到程序在進行初始化時,網絡未連接或設備時間未同步的問題。這兩個問題都會導致登錄失敗。所以登錄之前先檢查一下網絡是否已經可用,時間是否已經同步。這里只是判斷了時間中的year部分是否包含”201″。

    private Timer checkNetTimer = new Timer();
    private void checkNetAvailable(){
        checkNetTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                Runtime runtime = Runtime.getRuntime();
                Process pingProcess = null;
                try {
                    String nowDate = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date());
                    //時間是否已經同步
                    if(nowDate.contains("201")){
                        checkNetTimer.cancel();
                        InterfaceUrls.demoLogin(MLOC.userId);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },3000,3000);
    }

上邊代碼中InterfaceUrls.demoLogin(MLOC.userId)是向服務器獲取登錄證明AuthKey的,拿到證明之后就是真正的登錄了。這里我通過自定義的事件來專遞各種消息和參數。
登錄SDK成功后,小車將處於待命狀態,等待操控者的啟動指令。這里使用的啟動指令是“IotCarStart”這個字符串。

@Override
    public void dispatchEvent(String aEventID, boolean success, Object eventObj) {
        switch (aEventID){
            case AEvent.AEVENT_LOGIN:
                if(success){
                    MLOC.d("", (String) eventObj);
                    //登錄SDK
                    XHClient.getInstance().getLoginManager().login(MLOC.authKey, new IXHCallback() {
                        @Override
                        public void success(Object data) {
                            isLogin = true;
                        }

                        @Override
                        public void failed(final String errMsg) {
                            MLOC.d("",errMsg);
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    MLOC.showMsg(SplashActivity.this,errMsg);
                                }
                            });
                        }
                    });
                }else{
                    MLOC.d("", (String) eventObj);
                }
                break;
            case AEvent.AEVENT_C2C_REV_MSG:
                XHIMMessage message = (XHIMMessage) eventObj;
                String command = message.contentData;
                switch (command){
                    case "IotCarStart":
                        startCar(message.fromId);
                        break;
                }
                break;
        }
    }

    private void startCar(String fromId){
        removeListener();
        Intent intent = new Intent(SplashActivity.this,SampleLiveActivity.class);
        intent.putExtra("driverId",fromId);
        startActivity(intent);
    }

3.2 小車收到開機指令
當手機端通過IM,向小車發送一條內容為“IotCarStart”的一對一消息時,小車就會啟動,並且記錄操控者的ID,直到本次遙控結束,小車將忽略其他人發來的一切消息。
小車啟動時,要創建一個互動直播的房間,並開始直播。開始直播后,通過一對一消息將直播間的id發給手機端。手機端通過直播間id進入直播間,並申請連麥。小車收到連麥申請后判斷是否是操控者發來的,是操控者將允許連麥,否則拒絕連麥。

     private void createLive(){
        //創建直播
        final XHLiveItem liveItem = new XHLiveItem();
        liveItem.setLiveType(XHConstants.XHLiveType.XHLiveTypeGlobalPublic);
        liveItem.setLiveName(MLOC.userId);
        //創建直播間
        liveManager.createLive(liveItem, new IXHCallback() {
            @Override
            public void success(Object data) {
                //創建成功
                MLOC.d("XHLiveManager","createLive success "+data);
                liveId = (String) data;
                MLOC.saveSharedData(SampleLiveActivity.this,MLOC.userId+"_iotCarId", liveId);
                InterfaceUrls.demoReportLive(liveId,liveItem.getLiveName(),MLOC.userId);
                starLive();
            }
            @Override
            public void failed(final String errMsg) {
                //創建失敗
                MLOC.d("XHLiveManager","createLive failed "+errMsg);
                removeListener();
                finish();
            }
        });
    }

    private void starLive(){
        //開始直播
        liveManager.startLive(liveId, new IXHCallback() {
            @Override
            public void success(Object data) {
                //成功
                MLOC.d("XHLiveManager","startLive success "+data);
                //給操控者發送直播間ID
                XHClient.getInstance().getChatManager().sendOnlineMessage(liveId, driverId, null);
            }
            @Override
            public void failed(final String errMsg) {
                //失敗
                MLOC.d("XHLiveManager","startLive failed "+errMsg);
                MLOC.saveSharedData(SampleLiveActivity.this,MLOC.userId+"_iotCarId", "");
                stop();
            }
        });
    }
 @Override
    public void dispatchEvent(String aEventID, boolean success, final Object eventObj) {
        MLOC.d("XHLiveManager","dispatchEvent  "+aEventID + eventObj);
        switch (aEventID){
            case AEvent.AEVENT_C2C_REV_MSG:
                XHIMMessage message = (XHIMMessage) eventObj;
                if(message.fromId.equals(driverId)){
                    String command = message.contentData;
                    if(command.equals("IotCarStart")){
                        XHClient.getInstance().getChatManager().sendOnlineMessage(liveId, driverId, null);
                    }
                }
                break;
            case AEvent.AEVENT_LIVE_ADD_UPLOADER:
                //連麥者加入,因為小車不需要播放,所以設置為不接收視頻
                StarRtcCore.getInstance().setNullVideo();
                break;
            case AEvent.AEVENT_LIVE_REMOVE_UPLOADER:
                //操控者退出,本次操控結束
                driverId = "";
                stop();
                break;
            case AEvent.AEVENT_LIVE_APPLY_LINK:
                //收到連麥申請
                if(driverId.equals((String) eventObj)){
                    // 操控者申請,自動同意上麥
                    liveManager.agreeApplyToBroadcaster(driverId);
                }else{
                    // 拒絕其他人上麥
                    liveManager.refuseApplyToBroadcaster((String) eventObj);
                }
                break;
            case AEvent.AEVENT_LIVE_ERROR:
                removeListener();
                finish();
                MLOC.d("VideoLiveActivity","AEVENT_LIVE_ERROR  "+eventObj);
                break;
            case AEvent.AEVENT_LIVE_REV_REALTIME_DATA:
                // 收到實時流數據,操控車的指令
                if(success){
                    try {
                        JSONObject jsonObject = (JSONObject) eventObj;
                        final byte[] tData = (byte[]) jsonObject.get("data");
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                //給小車下達指令
                                GpioManager.getInstance().controlCar(tData);
                            }
                        });
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
    }

4 通過樹莓派GPIO和PWM控制小車運動
4.1 GPIO控制驅動電機
我購買的直流電機驅動板可以直接插在樹莓派上,並將樹莓的引腳再次暴露出來,用起來比較方便。
根據驅動板使用說明,將電機線接到驅動板上,然后通過GPIO設置相應引腳的高低電平就能控制電機的啟動停止和正轉反轉。
使用GPIO要記得先申請相關權限
GPIO使用前記得重置,使用后記得銷毀,GPIO口一旦被占用,后來者將無法使用。

    //初始化小車需要的GPIO口
    public void initCarGpio(){
        manager = PeripheralManager.getInstance();
        try {
            mGpioLeftRun = manager.openGpio(GpioNameLeftRun);
            resetGpio(mGpioLeftRun);
            mGpioLeftDirection = manager.openGpio(GpioNameLeftDirection);
            resetGpio(mGpioLeftDirection);
            mGpioRightRun = manager.openGpio(GpioNameRightRun);
            resetGpio(mGpioRightRun);
            mGpioRightDirection = manager.openGpio(GpioNameRightDirection);
            resetGpio(mGpioRightDirection);
        } catch (IOException e) {
            MLOC.d("IOTCAR","initCarGpio IOException"+e.getMessage());
            e.printStackTrace();
        }
        MLOC.d("IOTCAR","initCarGpio");
    }

    //關閉車
    public void stopCarGpio(){
        destoryGpio(mGpioLeftRun);
        destoryGpio(mGpioLeftDirection);
        destoryGpio(mGpioRightRun);
        destoryGpio(mGpioRightDirection);

        MLOC.d("IOTCAR","stopCarGpio");
    }
    //重置GPIO
    private void resetGpio(Gpio gpio){
        try {
            if(gpio!=null) {
                gpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);//設置為輸出,默認低電平
                gpio.setActiveType(Gpio.ACTIVE_HIGH);//設置高電平為活躍的
                gpio.setValue(false);//設置成低電平
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //銷毀GPIO
    private void destoryGpio(Gpio gpio){
        try {
            if(gpio!=null){
                gpio.close();
                gpio = null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在這吐糟一下樹莓派和電機驅動板,樹莓派每次通電開機時,控制電機使能的兩個GPIO口默認輸出的都是高電平,導致小車每次開機時會不受控制的一直往前跑。直到開始運行程序,GPIO口被重置成低電平才會停下來…

4.2 PWM控制雲台舵機旋轉
攝像頭雲台的控制需要使用PWM,Android Things在樹莓派上提供了兩個可以生成PWM波的引腳,正好可以將雲台的兩個舵機接入到樹莓派的相應PWM引腳上,一個控制左右轉動,一個控制上上下轉動。

這里需要說一下自己碰到的坑,剛開始不知道Android Things提供了PWM的API,所以自己寫了一個SoftPWM。但是PWM的周期是20ms,控制舵機從0到180度轉動所需的高電平寬度是0.5ms-1.5ms,然而java的計時器最小單位就是ms,所以根本無法滿足舵機調節角度的精度需求。幸虧很快發現了系統提供的API。之后又遇到個坑,只要小車打開視頻,雲台就無法控制。后來定位到問題是,播放聲音時speaker也是通過PWM控制發聲的,也就是說PWM被系統拿去播放音頻了,所以雲台無法控制。解決這個問題只需要再加一個PWM發生器即可,我選擇了另一條路,禁用了小車的音頻輸出。

繼續說舵機和PWM,PWM是通過設置頻率和占空比來產生不通方波的,理論上舵機的0~180度對應的高電平寬是0.5ms~1.5ms,也就是0度的占空比 = 0.5ms/20ms = 2.5%,可能因為理論值和實際值有偏差,經過我的測試,設置占空比為3.27%時,也就是高電平寬w = 3.27%*20ms = 0.654ms時,舵機的角度為0度。后邊又測出了180度時的占空比值,最終計算出角度每增加1度,占空比增加約0.0463。有了0度的基礎值和單步值,后邊設置角度時就比較方便了。
比如 90度時PWM的占空比=3.27+90*0.0463。

PWM最好持續輸出,時斷時續的容易造成舵機無規則擺動,停止遙控時一定要記得停止你使用的PWM,不然舵機一直工作,影響舵機使用壽命和電池續航。

    public void initCarPwm(){
        MLOC.d("IOTCAR_PWM","initCarPwm");
        manager = PeripheralManager.getInstance();
        try {
            if(mPwmCameraH!=null) {
                mPwmCameraH.setEnabled(false);
                mPwmCameraH.close();
                mPwmCameraH = null;
            }
            running.set(true);
            mPwmCameraH = manager.openPwm(GpioNameHRotate);
            mPwmCameraH.setPwmDutyCycle(beginValue+90*stepLenght);//設置占空比
            mPwmCameraH.setPwmFrequencyHz(50);//設置頻率 
            mPwmCameraH.setEnabled(true); //開始生成PWM

            if(mPwmCameraV!=null) {
                mPwmCameraV.setEnabled(false);
                mPwmCameraV.close();
                mPwmCameraV = null;
            }
            mPwmCameraV = manager.openPwm(GpioNameVRotate);
            mPwmCameraV.setPwmDutyCycle(beginValue+40*stepLenght);
            mPwmCameraV.setPwmFrequencyHz(50);
            mPwmCameraV.setEnabled(true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void changePwm(){
        try {
            if(mPwmCameraV!=null&&lastCameraV!=camearV.get()){
                mPwmCameraV.setEnabled(false);//先停掉之前的PWM
                mPwmCameraV.setPwmDutyCycle(beginValue+camearV.get()*stepLenght);//重新設置占空比
                mPwmCameraV.setEnabled(true);//開始生成PWM
                lastCameraV = camearV.get();
            }
            if(mPwmCameraH!=null&&lastCameraH!=camearH.get()){
                mPwmCameraH.setEnabled(false);
                mPwmCameraH.setPwmDutyCycle(beginValue+camearH.get()*stepLenght);
                mPwmCameraH.setEnabled(true);
                lastCameraH = camearH.get();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void stopPwm(){
        running.set(false);
        if(mPwmCameraH!=null){
            try {
                mPwmCameraH.setEnabled(false);
                mPwmCameraH.close();
                mPwmCameraH = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(mPwmCameraV!=null){
            try {
                mPwmCameraV.setEnabled(false);
                mPwmCameraV.close();
                mPwmCameraV = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        MLOC.d("IOTCAR_PWM","stopPwm");
    }

到這里小車端的程序介紹完了,后邊再說說手機端。

同行的認可是遠行最大的動力,歡迎轉載本博客文章,轉載請注明出處,十分感謝。

StarRTC , AndroidThings , 樹莓派小車,公網環境,視頻遙控(一)准備工作
StarRTC , AndroidThings , 樹莓派小車,公網環境,視頻遙控(二)小車端
StarRTC , AndroidThings , 樹莓派小車,公網環境,視頻遙控(三)手機端
源碼下載地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM