大疆 DJI Mobile SDK 开发:模拟器调试

该博客围绕Android开发实现无人机飞行控制展开。先介绍创建飞行控制器界面,包括新建Activity、配置布局与代码等;接着阐述DJI Mobile SDK的回调方法;最后详细说明起飞、降落、返航等功能的实现,涉及获取控制器、调用API及设置返航高度等,还提及自定义提示。

目录

创建飞行控制器界面

1.新建Activiity

2.MainActivity

activity_main.xml

MainActivity.java

3.FlightActivity

activity_flight.xml

FlightActivity.java

4.AndroidManifest

DJI Mobile SDK的回调方法

 起飞、降落与返航的实现

获取控制器

API Reference:FlightController()

起飞与取消起飞

API Reference:startTaskoff()、cancelTaskoff()

降落与取消降落

API Reference:startLanding()、cancelLanding()

返航与取消返航

API Reference: startGoHome()、cancelGoHome()

返航高度的设置与获取

 API Reference: setGoHomeHeightInMeters()、getGoHomeHeightInMeters()

自定义提示


创建飞行控制器界面

1.新建Activiity

        在 “com.dji.importSDKDemo” 包上单击右键,新建一个名称为 FlightActivity、布局名为 activity_flight 的活动。

2.MainActivity

activity_main.xml

        在 MainActivity 布局文件 activity_main.xml 中添加一个名为 “飞行控制器” 的按钮

    <Button
        android:id="@+id/fight_controller"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginTop="15dp"
        android:text="飞行控制器"
        android:textSize="25sp"/>

MainActivity.java

        要在 MainActivity 中实现单击 “飞行控制器” 按钮跳转到 FlightActivity 页面的代码,即在 MainActivity.java 文件中的 initUI() 函数的最后加入如下代码

    private void initUI() {

        …………

        Button fight_controller = (Button) findViewById(R.id.fight_controller);
        fight_controller.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (checkDroneConnection() == false) {
                    return;
                }
                //弹出FlightActivity
                Intent i = new Intent(MainActivity.this, FlightActivity.class);
                startActivity(i);
            }
        });
    }

         在单击 “飞行控制器” 按钮跳转到 FlightActivity 页面前,需要判断应用程序激活注册、状态,以及无人机绑定、连接等是否正常。故在 initUI() 方法后添加 checkDroneConnection() 方法来判断,代码如下

    private boolean checkDroneConnection() {
        //应用程序激活管理器
        AppActivationManager mgrActivation = DJISDKManager.getInstance().getAppActivationManager();

        //判断应用程序是否注册
        if (!DJISDKManager.getInstance().hasSDKRegistered()) {
            showToast("应用程序未注册");
            return false;
        }

        //判断应用程序是否激活
        if (mgrActivation.getAppActivationState() != AppActivationState.ACTIVATED) {
            showToast("应用程序未激活");
            return false;
        }

        //判断无人机是否绑定
        if (mgrActivation.getAircraftBindingState() != AircraftBindingState.BOUND) {
            showToast("无人机未绑定");
            return false;
        }

        //判断无人机是否连接
        //if ((DJISDKManager.getInstance().getProduct() == null) || !(DJISDKManager.getInstance().getProduct().isConnected())) {
        BaseProduct product = DJISDKManager.getInstance().getProduct();
        if (product == null || !product.isConnected()) {
            showToast("无人机连接失败");
            return false;
        }
        return true;
    }

3.FlightActivity

activity_flight.xml

        在 FlightActivity UI界面添加名为 “起飞”、“取消起飞”、“降落”、“取消降落”、“设置返航高度”、“获取返航高度”、“返航”、“取消返航” 按钮,界面与代码如下:

FlightActivity UI界面
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="10dp" >
  <TextView
      android:id="@+id/DJI_fly"
      style="?android:listSeparatorTextViewStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="飞行控制"
      android:textSize="15sp"
      app:layout_constraintTop_toTopOf="parent"
      android:layout_marginTop="10dp" />

  <Button
      android:id="@+id/btn_takeoff"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="起飞"
      app:layout_constraintTop_toBottomOf="@+id/DJI_fly"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_takeoff_cancel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="取消起飞"
      app:layout_constraintTop_toBottomOf="@+id/btn_takeoff"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_landing"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="降落"
      app:layout_constraintTop_toBottomOf="@+id/btn_takeoff_cancel"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_landing_cancel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="取消降落"
      app:layout_constraintTop_toBottomOf="@+id/btn_landing"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_set_home_height"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="设置返航高度"
      app:layout_constraintTop_toBottomOf="@+id/btn_landing_cancel"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_get_home_height"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="获取返航高度"
      app:layout_constraintTop_toBottomOf="@+id/btn_set_home_height"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_home"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="返航"
      app:layout_constraintTop_toBottomOf="@+id/btn_get_home_height"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
  <Button
      android:id="@+id/btn_home_cancel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="取消返航"
      app:layout_constraintTop_toBottomOf="@+id/btn_home"
      android:layout_marginTop="10dp"
      android:textSize="25sp"
      android:padding="15dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

FlightActivity.java

        在上一步添加的按钮,要使其实现功能,需要在 FlightActivity.java获取上述8个按钮的对象,并监听其单击方法,代码如下:

public class FlightActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mBtnTakeoff, mBtnCancelTakeoff, mBtnLanding, mBtnCancelLanding;
    private Button mBtnSetHomeHeight, mBtnGetHomeHeight, mBtnHome, mBtnCancelHome;

    public FlightActivity() {
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flight);
        //初始化UI界面
        initUI();
        //初始化监听器
        initListener();
    }

    private void initUI() {
        mBtnTakeoff = findViewById(R.id.btn_takeoff);
        mBtnCancelTakeoff = findViewById(R.id.btn_takeoff_cancel);
        mBtnLanding = findViewById(R.id.btn_landing);
        mBtnCancelLanding = findViewById(R.id.btn_landing_cancel);
        mBtnSetHomeHeight = findViewById(R.id.btn_set_home_height);
        mBtnGetHomeHeight = findViewById(R.id.btn_get_home_height);
        mBtnHome = findViewById(R.id.btn_home);
        mBtnCancelHome = findViewById(R.id.btn_home_cancel);
    }

    private void initListener() {
        mBtnTakeoff.setOnClickListener(this);
        mBtnCancelTakeoff.setOnClickListener(this);
        mBtnLanding.setOnClickListener(this);
        mBtnCancelLanding.setOnClickListener(this);
        mBtnSetHomeHeight.setOnClickListener(this);
        mBtnGetHomeHeight.setOnClickListener(this);
        mBtnHome.setOnClickListener(this);
        mBtnCancelHome.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_takeoff: {
                takeoff();
                break;
            }
            case R.id.btn_takeoff_cancel: {
                cancelTakeoff();
                break;
            }
            case R.id.btn_landing: {
                landing();
                break;
            }
            case R.id.btn_landing_cancel: {
                cancelLanding();
                break;
            }
            case R.id.btn_set_home_height: {
                setHomeHeight();
                break;
            }
            case R.id.btn_get_home_height: {
                getHomeHeight();
                break;
            }
            case R.id.btn_home: {
                home();
                break;
            }
            case R.id.btn_home_cancel: {
                cancelHome();
                break;
            }
            default:
                break;
        }
    }

    //起飞
    private void takeoff() {}

    //取消起飞
    private void cancelTakeoff() {}

    //降落
    private void landing() {}

    //取消降落
    private void cancelLanding() {}

    //设置返航高度
    private void setHomeHeight() {}

    //获取返航高度
    private void getHomeHeight() {}

    //返航
    private void home() {}

    //取消返航
    private void cancelHome() {}

    //在主线程中显示提示
    private void showToast(final String toastMsg) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), toastMsg, Toast.LENGTH_LONG).show();
            }
        });
    }
}

4.AndroidManifest

        在 AndroidManifest.xml 文件中,配置 MainActivity FlightActivity 之间的关系,以便于在 FlightActivity 中返回到 MainActivity,界面与代码如下:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation"
    android:launchMode="singleTop"
    android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity
    android:name=".FlightActivity"
    android:configChanges="orientation"
    android:parentActivityName=".MainActivity"
    android:screenOrientation="portrait" />

DJI Mobile SDK的回调方法

无人机的监控操作需要通过LightBridge、OcuSync等链路进行信息传递。

受信号衰减和环境干扰的影响,信息传递通常要消耗一定时间完成。因此对无人机的各类操作均需要通过回调的方法来介入处理。

 在 Android Mobile SDK 中,回调函数的接口通过 CommonCallbacks 定义

  • CompletionCallback :仅包括 onResult(DJIError  error) 抽象方法。当 error 为空时,任务执行成功;当 error 不为空时,通过其 getDescription() 方法详细错误信息
  • CompletionCallbackWith<T>:包括 onSuccess(T  val) onFailure(DJIError  error) 两个方法。当任务执行成功时,回调 onSuccess(…) 方法,且返回参数 val当任务执行失败时,回调 onFailure(…) 方法,且其中的 error 对象包含了错误描述
  • CompletionCallbackWithTwoParam<X, Y>:与 CompletionCallbackWith<T> 类似,包括 onSuccess(X val1, Y val2) onFailure(DJIError  error) 两个方法,方法执行成功时返回 val1 和 val2 两个参数

 起飞、降落与返航的实现

获取控制器

API Reference:FlightController() 

“点击API可跳转DJI Developer官网API相应位置 ”

创建获取飞行控制器的 getFlightController() 方法

大疆Mobile SDK API中对FlightController() 类的描述


飞行控制器的功能由飞行控制器类定义,飞行控制器对象可通过以下步骤获取:

(1)通过 DJISDKManager 获取产品基类(BaseProduct)对象,并判断是否为无人机对象

(2)在 Android 中需要通过飞行控制器对象的 isConnected() 方法判断是否与无人机成功连接

具体代码实现:

    //获取无人机的飞行控制器
    private FlightController getFlightController() {
        BaseProduct product = DJISDKManager.getInstance().getProduct();
        if (product != null && product.isConnected()) {
            if (product instanceof Aircraft) {
                return ((Aircraft) product).getFlightController();
            }
        }
        return null;
    }

起飞与取消起飞

API Reference:startTaskoff()cancelTaskoff()

“点击API可跳转DJI Developer官网API相应位置 ”

有关 startTakeoff(…) 方法的描述
有关 cancelTakeoff(…) 方法的描述题

通过飞行控制器的自动起飞自动精准起飞方法可实现无人机的起飞动作。

自动起飞方法必须在电机关闭时调用,并在起飞后0.5m左右悬停并回调。对于具有下方视觉定位的无人机,可通过自动精准起飞方法在起飞的6m之内获得其周围视觉定位记忆,当无人机再次降落时即可降落到相对更加准确的位置。

具体代码实现:

        实现 takeoff()cancelTakeoff() 方法,通过飞行控制器对象的 startTakeoff(…) 方法实现起飞功能,通过飞行控制器对象的 cancelTakeoff(…) 方法实现取消起飞功能

    //起飞
    private void takeoff() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.startTakeoff(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast(djiError.toString());
                    } else {
                        showToast("开始起飞!");
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

    //取消起飞
    private void cancelTakeoff() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.cancelTakeoff(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast(djiError.toString());
                    } else {
                        showToast("取消起飞成功!");
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

降落与取消降落

API Reference:startLanding()cancelLanding()

“点击API可跳转DJI Developer官网API相应位置 ”

有关 startLanding(…) 方法的描述
有关 cancelLanding(…) 方法的描述

通过飞行控制器的自动降落方法可实现无人机的降落动作。

对于具有降落保护功能(下方避障功能)的无人机,无人机在距离地面 0.3m 时会对地面进行检测。如果检测通过,则自动进行降落;如果检测不通过,则需要通过确认自动降落的方法继续执行降落程序。

        实现 landing()cancelLanding() 方法,通过飞行控制器对象的 startLanding(…) 方法实现降落功能,通过飞行控制器的 cancelLanding(…) 方法实现取消降落功能。

具体代码实现: 

    //降落
    private void landing() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.startLanding(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast("开始降落!");
                    } else {
                        showToast(djiError.toString());
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

    //取消降落
    private void cancelLanding() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.cancelLanding(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast("取消降落成功!");
                    } else {
                        showToast(djiError.toString());
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

        在 Android 中,是否需要确认降落需要通过飞行控制器状态(FlightControllerState)的isLandingConfirmationNeeded() 方法获取。如需要确认降落,则可通过飞行控制器类的confirmLanding(…) 方法继续降落。


返航与取消返航

API Reference: startGoHome()cancelGoHome()

“点击API可跳转DJI Developer官网API相应位置 ”

有关 startGoHome(…) 方法的描述
有关 cancelGoHome(…) 方法的描述

        实现 home()cancelhome() 方法,通过飞行控制器对象的 startGoHome(…) 方法实现返航功能,通过飞行控制器的 cancelGoHome(…) 方法实现取消返航功能。 

具体代码实现:

    //返航
    private void home() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.startGoHome(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast("开始返航!");
                    } else {
                        showToast(djiError.toString());
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

    //取消返航
    private void cancelHome() {
        FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.cancelGoHome(new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    if (djiError != null) {
                        showToast("取消返航成功!");
                    } else {
                        showToast(djiError.toString());
                    }
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

返航高度的设置与获取

 API Reference:  setGoHomeHeightInMeters()getGoHomeHeightInMeters()

 “点击API可跳转DJI Developer官网API相应位置 ”

有关 setGoHomeHeightInMeters(…) 方法的描述

返航高度应设置在20~500m 的范围内,切不超过得醒的最大高度。

当返航高度设置过低时,会返回类 “The go home altitude is too low (lower than 20m)” 的错误提示;当设置的返航高度过高时,会返回类似 “The go home altitude is too high (higher than max flight height)” 的错误提示。

有关 getGoHomeHeightInMeters(…) 方法的描述

        实现 setGoHomeHeight() getGoHomeHeight() 方法,通过飞行控制器对象的 setGoHomeHeightInMeters(…) 方法实现设置返航高度功能,通过飞行控制器的 getGoHomeHeightInMeters(…) 方法实现获取返航高度功能。 

具体代码实现:

    //设置返航高度
    private void setHomeHeight() {
        //设置返航高度文本里
        final EditText editText = new EditText(this);
        //限定只能输入数字
        editText.setInputType(InputType.TYPE_CLASS_NUMBER);
        new AlertDialog.Builder(this)
                .setTitle("请输入返航高度(m)")
                .setView(editText)
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        int height = Integer.parseInt(editText.getText().toString());
                        final FlightController flightController = getFlightController();
                        if (flightController != null) {
                            flightController.setGoHomeHeightInMeters(height, new CommonCallbacks.CompletionCallback() {
                                @Override
                                public void onResult(DJIError djiError) {
                                    if (djiError != null) {
                                        showToast(djiError.toString());
                                    } else {
                                        showToast("返航高度设置成功!");
                                    }
                                }
                            });
                        } else {
                            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
                        }
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    //获取返航高度
    private void getHomeHeight() {
        final FlightController flightController = getFlightController();
        if (flightController != null) {
            flightController.getGoHomeHeightInMeters(new CommonCallbacks.CompletionCallbackWith<Integer>() {
                @Override
                public void onSuccess(Integer integer) {
                    showToast("返航高度为:" + integer + "米");
                }

                @Override
                public void onFailure(DJIError djiError) {
                    showToast("获取返航高度失败:" + djiError.toString());
                }
            });
        } else {
            showToast("飞行控制器获取失败,请检查飞行控制器是否连接正常!");
        }
    }

        在 setGoHomeHeight() 方法中,通过 AlertDialog 类弹出对话框,用于用户输入返航高度,由于 setView(editText) 语句,将输入内容限定为整形数字

设置返航高度对话框

         在关闭对话框后,调用飞行控制器的 setGoHomeHeightInMeters(…) 方法,并传入返航高度和回调函数。getGoHomeHeight() 方法中,其回调方法 onSuccess(Integer height) 中的 height 参数返回了无人机的返航高度


自定义提示

        代码中均使用 showToast() 方法来实现在主线程中显示提示的功能,也可直接使用 Toast() 提示,将文中代码涉及 showToast() 的部分更换为 Toast() 

    //在主线程中显示提示
    private void showToast(final String toastMsg) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), toastMsg, Toast.LENGTH_LONG).show();
            }
        });
    }

按住键盘的 Crtl 键,鼠标左键单击Toast方法,即可在 Android Studio 中跳转到 Toast 类 

public class Toast {
    public static final int LENGTH_LONG = 1;
    public static final int LENGTH_SHORT = 0;

    public Toast(Context context) {
        throw new RuntimeException("Stub!");
    }

    public void show() {
        throw new RuntimeException("Stub!");
    }

    public void cancel() {
        throw new RuntimeException("Stub!");
    }

    /** @deprecated */
    @Deprecated
    public void setView(View view) {
        throw new RuntimeException("Stub!");
    }

    /** @deprecated */
    @Deprecated
    @Nullable
    public View getView() {
        throw new RuntimeException("Stub!");
    }

    public void setDuration(int duration) {
        throw new RuntimeException("Stub!");
    }

    public int getDuration() {
        throw new RuntimeException("Stub!");
    }

    public void setMargin(float horizontalMargin, float verticalMargin) {
        throw new RuntimeException("Stub!");
    }

    public float getHorizontalMargin() {
        throw new RuntimeException("Stub!");
    }

    public float getVerticalMargin() {
        throw new RuntimeException("Stub!");
    }

    public void setGravity(int gravity, int xOffset, int yOffset) {
        throw new RuntimeException("Stub!");
    }

    public int getGravity() {
        throw new RuntimeException("Stub!");
    }

    public int getXOffset() {
        throw new RuntimeException("Stub!");
    }

    public int getYOffset() {
        throw new RuntimeException("Stub!");
    }

    public void addCallback(@NonNull Toast.Callback callback) {
        throw new RuntimeException("Stub!");
    }

    public void removeCallback(@NonNull Toast.Callback callback) {
        throw new RuntimeException("Stub!");
    }

    public static Toast makeText(Context context, CharSequence text, int duration) {
        throw new RuntimeException("Stub!");
    }

    public static Toast makeText(Context context, int resId, int duration) throws NotFoundException {
        throw new RuntimeException("Stub!");
    }

    public void setText(int resId) {
        throw new RuntimeException("Stub!");
    }

    public void setText(CharSequence s) {
        throw new RuntimeException("Stub!");
    }

    public abstract static class Callback {
        public Callback() {
            throw new RuntimeException("Stub!");
        }

        public void onToastShown() {
            throw new RuntimeException("Stub!");
        }

        public void onToastHidden() {
            throw new RuntimeException("Stub!");
        }
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hsupering

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值