原文地址: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 , 樹莓派小車,公網環境,視頻遙控(三)手機端
源碼下載地址
