编写Android中的蓝牙模块驱动和底层HART设备


【背景】

手上有个android设备:

现在希望可以在PAD上通过蓝牙去连接蓝牙的HART猫,然后再去操作HART的设备。

现在就是去android中写对应的蓝牙模块的程序,此处暂且叫做蓝牙驱动吧。

其中此处的开发环境是ADT。

【折腾过程】

1.参考:

Bluetooth | Android Developers

去添加权限:

add user permission android.permission.BLUETOOTH

添加好了:

added android.permission.BLUETOOTH and BLUETOOTH_ADMIN

2.

参考代码期间,遇到:

【已解决】Android的蓝牙实例代码中找不到REQUEST_ENABLE_BT

然后再去用BluetoothAdapter去检测是否支持蓝牙,且支持的话去打开:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private final int REQUEST_ENABLE_BT = 1 ;
private BluetoothAdapter mBluetoothAdapter;
private void testBluetooth() {
     mBluetoothAdapter= BluetoothAdapter.getDefaultAdapter();
 
     if (mBluetoothAdapter == null ) {
         // Device does not support Bluetooth
         Toast.makeText(getApplicationContext(), "Device does not support Bluetooth" , Toast.LENGTH_LONG).show();
     }
     else {
         //android.bluetooth.BluetoothAdapter@211120b0
         Toast.makeText(getApplicationContext(), "Device support Bluetooth" , Toast.LENGTH_SHORT).show();
         
         if (!mBluetoothAdapter.isEnabled()) {
             Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
             startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
         }
         else {
             scanOrDiscoverBtDevices();
         }
     }
 
}
 
@Override
protected void onActivityResult( int requestCode, int resultCode, Intent data) {
     if (requestCode == REQUEST_ENABLE_BT)
     {
         if (resultCode == RESULT_OK){
             Toast.makeText(getApplicationContext(), "Enabled Bluetooth now" , Toast.LENGTH_LONG).show();
             scanOrDiscoverBtDevices();
         }
         else {
             Toast.makeText(getApplicationContext(), "Not enable Bluetooth !" , Toast.LENGTH_LONG).show();
         }
     }
}

3.关于检测蓝牙状态变化,而去实现蓝牙被打开还是关闭了,可以参考:

Optionally, your application can also listen for the ACTION_STATE_CHANGED broadcast Intent, which the system will broadcast whenever the Bluetooth state has changed. This broadcast contains the extra fields EXTRA_STATE and EXTRA_PREVIOUS_STATE, containing the new and old Bluetooth states, respectively. Possible values for these extra fields areSTATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF, and STATE_OFF. Listening for this broadcast can be useful to detect changes made to the Bluetooth state while your app is running.

4.另外,看到提示了:

Tip: Enabling discoverability will automatically enable Bluetooth. If you plan to consistently enable device discoverability before performing Bluetooth activity, you can skip step 2 above. Read about enabling discoverability, below.

这也就是我之前疑惑的:

别的那个android的app,可以点击开启蓝牙,而无需弹出请求权限的对话框。

就是通过这个discovery功能实现的。

 

提示:

后来又遇到:

【已解决】Android中运行startActivityForResult后但是onActivityResult不执行

 

5.目前已经实现了,既可以scan那些paired,也可以discover搜寻当前附近的蓝牙设备。

代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
private final int REQUEST_ENABLE_BT = 1 ;
private BluetoothAdapter mBluetoothAdapter;
private void testBluetooth() {
     mBluetoothAdapter= BluetoothAdapter.getDefaultAdapter();
 
     if (mBluetoothAdapter == null ) {
         // Device does not support Bluetooth
         Toast.makeText(getApplicationContext(), "Device does not support Bluetooth" , Toast.LENGTH_LONG).show();
     }
     else {
         //android.bluetooth.BluetoothAdapter@211120b0
         Toast.makeText(getApplicationContext(), "Device support Bluetooth" , Toast.LENGTH_SHORT).show();
         
         if (!mBluetoothAdapter.isEnabled()) {
             Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
             startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
         }
         else {
             scanOrDiscoverBtDevices();
         }
     }
 
}
 
private void scanOrDiscoverBtDevices(){
     //scanBtDevices();
     
     discoverBtDevices();
}
 
// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
     public void onReceive(Context context, Intent intent) {
         String action = intent.getAction();
         // When discovery finds a device
         if (BluetoothDevice.ACTION_FOUND.equals(action)) {
             // Get the BluetoothDevice object from the Intent
             BluetoothDevice btDev = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
             //mArrayAdapter.add(btDev.getName() + "\n" + btDev.getAddress());
             Toast.makeText(getApplicationContext(), btDev.getName() + "\n" + btDev.getAddress(), Toast.LENGTH_LONG).show();
         }
     }
};
 
private void discoverBtDevices(){
     // Register the BroadcastReceiver
     IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
     registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
     
     mBluetoothAdapter.startDiscovery();
}
 
private void scanBtDevices(){
     Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); //[BC:85:1F:96:99:C9, 00:06:66:4C:75:FE]
  // If there are paired devices
  if (pairedDevices.size() > 0 ) {
      //ArrayAdapter mArrayAdapter = new ArrayAdapter();
      
      // Loop through paired devices
      for (BluetoothDevice btDev : pairedDevices) {
          //mArrayAdapter.add(btDev.getName() + "\n" + btDev.getAddress());
          Toast.makeText(getApplicationContext(), btDev.getName() + "\n" + btDev.getAddress(), Toast.LENGTH_LONG).show();
      }
  }
}
 
@Override
protected void onActivityResult( int requestCode, int resultCode, Intent data) {
     if (requestCode == REQUEST_ENABLE_BT)
     {
         if (resultCode == RESULT_OK){
             Toast.makeText(getApplicationContext(), "Enabled Bluetooth now" , Toast.LENGTH_LONG).show();
             scanOrDiscoverBtDevices();
         }
         else {
             Toast.makeText(getApplicationContext(), "Not enable Bluetooth !" , Toast.LENGTH_LONG).show();
         }
     }
}

6.继续折腾。

对于可以被搜索“Enabling discoverability”暂时就不去关心了。毕竟当前android设备能搜到HART猫即可。暂时无需考虑被搜索到。

7.再去建立连接,期间出现UUID和connect失败方面的问题:

【已解决】Android中连接蓝牙设备时遇到createRfcommSocketToServiceRecord的UUID问题和BluetoothSocket的connect失败

最终,用如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
private String mactekHartModemName;
private UUID mactekHartModemUuid;
 
//void afterFoundBtHartModem(BluetoothDevice btDev, Parcelable[] btDevUuid){
void afterFoundBtHartModem(BluetoothDevice btDev, UUID btDevUuid){
     if ( null != btDevUuid){
                     
     }
     
     //mactekHartModemName = btDev.getName(); //"MACTekViator75FE"
     //mactekHartModemUuid = UUID.fromString(mactekHartModemName);
     
     String uuidValue;
     //uuidValue = "e214d9ae-c3ba-4e25-abb5-299041353bc3";
     
     //https://groups.google.com/forum/#!topic/android-developers/vyTEJOXELos
     //uuidValue = "00001101-0000-100­0-8000-00805F9B34FB";
     //uuidValue = "00000003-0000-100­0-8000-00805F9B34FB";
     uuidValue = "00001101-0000-1000-8000-00805F9B34FB" ;
 
     mactekHartModemUuid = UUID.fromString(uuidValue);
 
     ConnectThread connectBtThread = new ConnectThread(btDev);
     connectBtThread.start();
}
 
private class ConnectThread extends Thread {
     private final BluetoothSocket mmSocket;
     private final BluetoothDevice mmDevice;
  
     public ConnectThread(BluetoothDevice device) {
         // Use a temporary object that is later assigned to mmSocket,
         // because mmSocket is final
         BluetoothSocket tmp = null ;
         mmDevice = device;
  
         // Get a BluetoothSocket to connect with the given BluetoothDevice
         try {
             // MY_UUID is the app's UUID string, also used by the server code
             tmp = device.createRfcommSocketToServiceRecord(mactekHartModemUuid); //00001101-0000-1000-8000-00805F9B34FB
 
         } catch (IOException e) { }
         mmSocket = tmp;
     }
  
     public void run() {
         // Cancel discovery because it will slow down the connection
         mBluetoothAdapter.cancelDiscovery();
  
         try {
             // Connect the device through the socket. This will block
             // until it succeeds or throws an exception
             mmSocket.connect();
         } catch (IOException connectException) {
             // Unable to connect; close the socket and get out
             try {
                 mmSocket.close();
             } catch (IOException closeException) { }
             return ;
         }
  
         // Do work to manage the connection (in a separate thread)
         manageConnectedSocket(mmSocket);
     }
  
     /** Will cancel an in-progress connection, and close the socket */
     public void cancel() {
         try {
             mmSocket.close();
         } catch (IOException e) { }
     }
}

可以正常触发android系统打开配对窗口,输入密码:

pair with MACTekViator75FE bluetooth hart modem

后,是可以配对成功的:

use uuid 00001101-0000-1000-8000-00805F9B34FB then bluetooth connect will ok

8.然后接着再去试试发送数据给蓝牙HART猫是否成功,能否收到返回的数据。

最后所用代码为:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
     private final int REQUEST_ENABLE_BT = 1 ;
     private BluetoothAdapter mBluetoothAdapter;
     private void testBluetooth() {
         mBluetoothAdapter= BluetoothAdapter.getDefaultAdapter();
 
         if (mBluetoothAdapter == null ) {
             // Device does not support Bluetooth
             Toast.makeText(getApplicationContext(), "Device does not support Bluetooth" , Toast.LENGTH_LONG).show();
         }
         else {
             //android.bluetooth.BluetoothAdapter@211120b0
             Toast.makeText(getApplicationContext(), "Device support Bluetooth" , Toast.LENGTH_SHORT).show();
             
             if (!mBluetoothAdapter.isEnabled()) {
                 Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                 startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
             }
             else {
                 scanOrDiscoverBtDevices();
             }
         }
     }
 
     private void scanOrDiscoverBtDevices(){
         //scanBtDevices();
         
         discoverBtDevices();
     }
     
     // Create a BroadcastReceiver for ACTION_FOUND
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
             // When discovery finds a device
             if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                 // Get the BluetoothDevice object from the Intent
                 BluetoothDevice btDev = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
 
                 //mArrayAdapter.add(btDev.getName() + "\n" + btDev.getAddress());
                  String btDevName = btDev.getName();
                  String btDevMacAddr = btDev.getAddress();
                 Toast.makeText(getApplicationContext(), btDevName + "\n" + btDevMacAddr, Toast.LENGTH_LONG).show();
 
                 if (btDevName.contains( "MACTekViator" )){ //MACTekViator75FE
                     afterFoundBtHartModem(btDev);
                 }
             }
         }
     };
 
     private void discoverBtDevices(){
         // Register the BroadcastReceiver
         IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
         registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
         
         mBluetoothAdapter.startDiscovery();
     }
     
     private void scanBtDevices(){
         Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
         //[BC:85:1F:96:99:C9, 00:06:66:4C:75:FE]
 
          // If there are paired devices
          if (pairedDevices.size() > 0 ) {
 
              // Loop through paired devices
              for (BluetoothDevice btDev : pairedDevices) {
                  //mArrayAdapter.add(btDev.getName() + "\n" + btDev.getAddress());
                  //Toast.makeText(getApplicationContext(), btDev.getName() + "\n" + btDev.getAddress(), Toast.LENGTH_LONG).show();
                  
                  String btDevName = btDev.getName();
                  String btDevMacAddr = btDev.getAddress();
                  Toast.makeText(getApplicationContext(), btDevName + "\n" + btDevMacAddr, Toast.LENGTH_LONG).show();
     
                 if (btDevName.contains( "MACTekViator" )){ //found our concerned Bluetooth HART Modem
                     afterFoundBtHartModem(btDev);
                     break ;
                 }
              }
      }
     }
     
     @Override
     protected void onActivityResult( int requestCode, int resultCode, Intent data) {
         if (requestCode == REQUEST_ENABLE_BT)
         {
             if (resultCode == RESULT_OK){
                 Toast.makeText(getApplicationContext(), "Enabled Bluetooth now" , Toast.LENGTH_LONG).show();
                 scanOrDiscoverBtDevices();
             }
             else {
                 Toast.makeText(getApplicationContext(), "Not enable Bluetooth !" , Toast.LENGTH_LONG).show();
             }
         }
     }
     
     private UUID mactekHartModemUuid;
     
     void afterFoundBtHartModem(BluetoothDevice btDev){
         String sspUuid;
         
         //http://developer.android.com/reference/android/bluetooth/BluetoothDevice.html#createRfcommSocketToServiceRecord%28java.util.UUID%29
         //Hint: If you are connecting to a Bluetooth serial board then try using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB.
         //However if you are connecting to an Android peer then please generate your own unique UUID.
         sspUuid = "00001101-0000-1000-8000-00805F9B34FB" ;
 
         mactekHartModemUuid = UUID.fromString(sspUuid);
 
         ConnectThread connectBtThread = new ConnectThread(btDev);
         connectBtThread.start();
     }
 
     private class ConnectThread extends Thread {
         private final BluetoothSocket mmSocket;
         private final BluetoothDevice mmDevice;
 
         public ConnectThread(BluetoothDevice device) {
             // Use a temporary object that is later assigned to mmSocket,
             // because mmSocket is final
             BluetoothSocket tmp = null ;
             mmDevice = device;
      
             // Get a BluetoothSocket to connect with the given BluetoothDevice
             try {
                 // MY_UUID is the app's UUID string, also used by the server code
                 tmp = device.createRfcommSocketToServiceRecord(mactekHartModemUuid); //00001101-0000-1000-8000-00805F9B34FB
 
             } catch (IOException e) { }
             mmSocket = tmp;
         }
      
         public void run() {
             // Cancel discovery because it will slow down the connection
             mBluetoothAdapter.cancelDiscovery();
      
             try {
                 // Connect the device through the socket. This will block
                 // until it succeeds or throws an exception
                 mmSocket.connect();
             } catch (IOException connectException) {
                 // Unable to connect; close the socket and get out
                 try {
                     mmSocket.close();
                 } catch (IOException closeException) { }
                 return ;
             }
      
             // Do work to manage the connection (in a separate thread)
             manageConnectedSocket(mmSocket);
         }
      
         /** Will cancel an in-progress connection, and close the socket */
         public void cancel() {
             try {
                 mmSocket.close();
             } catch (IOException e) { }
         }
     }   
 
     private void manageConnectedSocket(BluetoothSocket socket){
         //Toast.makeText(getApplicationContext(), "Bluetooth HART Modem Connected", Toast.LENGTH_LONG).show();
         ConnectedThread connectedThread = new ConnectedThread(socket);
         connectedThread.start();
 
         //command 0
         String command0Str = "FFFFFFFFFF0280000082" ;
         byte [] commnd0Bytes = HexString2Bytes(command0Str);
         connectedThread.write(commnd0Bytes);
         //read out command 0 response
         byte [] readoutBuffer = new byte [ 1024 ];
         
         //Note: here use DEBUG, so will take some time, so follow can read out real response data
         //if no DEBUG, just run through, will only get -1
         //readoutBuffer = connectedThread.read(); //[-1,
         readoutBuffer = connectedThread.read(); //[-1, -1, -1, -1, 6, -128, 0, 14, 0, 69, -2, 54, 2, 5, 5, 2, 13, 1, 3, 43, -11, -82, 122, 0, 0, 0, 0,
         parseHartCommand0Resp(readoutBuffer);
         //readoutBuffer = connectedThread.read();
     }
     
     private void parseHartCommand0Resp( byte [] command0RespBytes){
         //-1, -1, -1, -1, 6, -128, 0, 14, 0, 69, -2, 54, 2, 5, 5, 2, 13, 1, 3, 43, -11, -82, 122,
         //FF FF FF FF 06 80 00 0E 00 45 FE 36 02 05 02 0D 01 03 2B F5 AE 7A
         //(hex value) --->
         //FF FF FF FF 06 80 00 0E 00 45
         //FE ==254(expansion)
         //ManufactureIdentificationCode=36==Yamatake
         //ManufactureDeviceType=02
         //PreampleNumber=5
         //UniversalCommandRevision=5==HART5
         //DeviceSpecificCommandRevision=2
         //SoftwareRevision=13
         //HardwareRevision=1
         //DeviceFunctionFlag=3
         //DeviceIdNumber=2B F5 AE
         //CommonPracticeCommandRevision=7A
     }
 
     private boolean isReadoutRespOk = false ;
     
     private class ConnectedThread extends Thread {
         private final BluetoothSocket mmSocket;
         private final InputStream mmInStream;
         private final OutputStream mmOutStream;
      
         public ConnectedThread(BluetoothSocket socket) {
             mmSocket = socket;
             InputStream tmpIn = null ;
             OutputStream tmpOut = null ;
      
             // Get the input and output streams, using temp objects because
             // member streams are final
             try {
                 tmpIn = socket.getInputStream();
                 tmpOut = socket.getOutputStream();
             } catch (IOException e) { }
      
             mmInStream = tmpIn;
             mmOutStream = tmpOut;
         }
      
         public void run() {
//          byte[] buffer = new byte[1024];  // buffer store for the stream
//          int bytes; // bytes returned from read()
//  
//          // Keep listening to the InputStream until an exception occurs
//          while (true) {
//              try {
//                  // Read from the InputStream
//                  bytes = mmInStream.read(buffer);
//                  // Send the obtained bytes to the UI activity
//                  //mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
//                  if(bytes > 0){
//                      //Toast.makeText(getApplicationContext(), "Got HART Command 0 resp data: " + buffer.toString(), Toast.LENGTH_LONG).show();
//                      isReadoutRespOk = true;
//                     
//                      //-1, -1, -1, -1, 6, -128, 0, 14, 0, 69, -2, 54, 2, 5, 5, 2, 13, 1, 3, 43, -11, -82, 122,
//                      //parseHartCommand0ResponseData();
//                  }
//                  else{
//                      isReadoutRespOk = false;
//                  }
//
//              } catch (IOException e) {
//                  break;
//              }
//          }
         }
      
         /* Call this from the main activity to send data to the remote device */
         public void write(byte[] bytes) {
             try {
                 mmOutStream.write(bytes);
             } catch (IOException e) { }
         }
      
         public byte[] read() {
             byte[] buffer = new byte[1024];  // buffer store for the stream
             int bytes;
             try {
                 bytes = mmInStream.read(buffer);
             } catch (IOException e) {
                 
             }
             return buffer;
         }
      
         /* Call this from the main activity to shutdown the connection */
         public void cancel() {
             try {
                 mmSocket.close();
             } catch (IOException e) { }
         }
     }
     
     //copy from other code
     // string to hex array
     public byte [] HexString2Bytes(String hexStr) {
         if ( null == hexStr || 0 == hexStr.length()) {
             return null ;
         }
         byte [] ret = new byte [hexStr.length() / 2 ];
         byte [] tmp = hexStr.getBytes();
         for ( int i = 0 ; i < (tmp.length / 2 ); i++) {
             ret[i] = uniteBytes(tmp[i * 2 ], tmp[i * 2 + 1 ]);
         }
         return ret;
     }
     
     private  byte uniteBytes( byte src0, byte src1) {
         byte _b0 = Byte.decode( "0x" + new String( new byte [] { src0 }))
                 .byteValue();
         _b0 = ( byte ) (_b0 << 4 );
         byte _b1 = Byte.decode( "0x" + new String( new byte [] { src1 }))
                 .byteValue();
         byte result = ( byte ) (_b0 | _b1);
         return result;
     }
?
1
  

 

【总结】

至此,终于算是跑通了,可以通过:

平板上面的蓝牙,扫描找到蓝牙HART猫,然后通过HART猫发送command 0给HART设备,获得对应的信息:

command 0 can response data bytes from mactek viator bluetooth hart modem

 

注:

此处所用的HART设备和这里:

【记录】试用通过蓝牙操作HART设备的Android的程序:teknikol COMMANDER

是一样的:

所以上述代码解析后的信息,也是和之前一致的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值