前言
在上一篇中我们进行扫描设备的处理,本文中进行连接和发现服务的数据处理,运行效果图如下所示:
正文
现在我们在ScanSlice
扫描设备,选中一个设备进入MainAbilitySlice
,下面要对选中的设备进行处理,首先我们来做连接。
一、BlePeripheral回调
在之前我们写了一个BleCore,这里面是对扫描的封装,那么对于连接来说我们同样可以封装到这里,我们可以在BleCore
中写一个BleDeviceCallback
类,继承自BlePeripheralCallback
, 代码如下所示:
private class BleDeviceCallback extends BlePeripheralCallback { /** * 连接状态改变 * @param connectionState 状态码 */ @Override public void connectionStateChangeEvent(int connectionState) { } /** * 发现服务 * @param status 状态 */ @Override public void servicesDiscoveredEvent(int status) { } }
因为本文要做的事情是连接和发现服务,所以我们就先重写这两个方法,注意一点的是,蓝牙的操作都是在子线程中进行的,如果我们需要知道当前是否连接,则需要写一个接口用于回调到MainAbilitySlice
中,在core
包下新建一个BleCallback
接口,代码如下所示:
public interface BleCallback { /** * 设备的所有信息 * * @param info 信息 */ void deviceInfo(String info); /** * 连接状态 * * @param state true or false */ void connectionStateChange(boolean state); /** * 发现服务 * * @param services 服务列表 */ void servicesDiscovered(List<GattService> services); }
接口中定义了三个方法,通过注释我们清晰的知道都是什么作用,这里着重介绍第一个函数,这个函数会显示设备各个时候的状态信息,从连接之后的所有动作,如果我们需要保存设备的操作日志的话,可以通过这个来进行处理保存。
然后回到BleCore,声明变量和设置接口回调的方法:
private final BleDeviceCallback mBleDeviceCallback; private BleCallback bleCallback; private BlePeripheralDevice mDevice; private boolean mIsConnected; public BleCore(Context context) { ... //蓝牙设备类 mBleDeviceCallback = new BleDeviceCallback(); } public void setBleCallback(BleCallback bleCallback) { this.bleCallback = bleCallback; } private void deviceInfo(String info) { if (bleCallback != null) { bleCallback.deviceInfo(info); } } private void connectState(boolean state) { mIsConnected = state; if (bleCallback != null) { bleCallback.connectionStateChange(state); } } public void setDevice(BlePeripheralDevice device) { mDevice = device; }
这里就是对设备信息和连接做了一个处理,下面我们增加连接和断连的方法。
二、连接和断连
在BleCore中增加如下代码:
public boolean isConnected() { return mIsConnected; } public void connect() { if (mDevice == null) return; deviceInfo("连接设备..."); mDevice.connect(false, mBleDeviceCallback); } public void disconnect() { if (mDevice == null) return; deviceInfo("断开连接设备..."); mDevice.disconnect(); }
连接与断开连接,调用时会触发connectionStateChangeEvent()
方法。
三、连接状态回调
下面修改这个方法的代码,如下所示:
@Override public void connectionStateChangeEvent(int connectionState) { String address = mDevice.getDeviceAddr(); if (connectionState == ProfileBase.STATE_CONNECTED) { deviceInfo("连接成功:" + address); connectState(true); } else if (connectionState == ProfileBase.STATE_DISCONNECTED) { deviceInfo("断开连接成功:" + address); connectState(false); mDevice.close(); } }
在回调中,连接成功和断开连接都会有一个对应的状态码,通过状态回调到接口函数中,然后回到MainAbilitySlice
中使用一下这个回调,首先我们修改一下ability_main.xml
中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:alignment="center" ohos:background_element="$color:bg_color" ohos:orientation="vertical"> <DirectionalLayout ohos:height="50vp" ohos:width="match_parent" ohos:alignment="vertical_center" ohos:background_element="$color:blue" ohos:orientation="horizontal" ohos:start_padding="12vp"> <Text ohos:height="match_content" ohos:width="match_content" ohos:text="操作设备" ohos:text_color="#FFF" ohos:text_font="HwChinese-medium" ohos:text_size="18fp" ohos:weight="1"/> <Text ohos:id="$+id:tx_disconnect" ohos:height="match_content" ohos:width="match_content" ohos:end_margin="6vp" ohos:padding="8vp" ohos:text="断开连接" ohos:text_color="#FFF" ohos:text_size="14fp"/> </DirectionalLayout> <Text ohos:id="$+id:tx_device_info" ohos:height="match_content" ohos:width="match_parent" ohos:text="设备信息" ohos:padding="12vp" ohos:text_size="14fp"/> <ListContainer ohos:id="$+id:lc_service" ohos:height="match_parent" ohos:width="match_parent"/> </DirectionalLayout>
在XML中只增加了两个Text,分别用于断连和显示设备状态,下面我们写Slice之间的跳转,也就是从ScanSlice
跳转到MainAbilitySlice
。首先我们在MainAbility
中增加action
,代码如下所示:
public class MainAbility extends Ability { @Override public void onStart(Intent intent) { ... // add action for ability addActionRoute("action.main", MainAbilitySlice.class.getName()); } }
然后去config.json中增加配置,如下图所示:
然后我们回到ScanSlice中,在Item的点击事件中添加如下代码:
//列表item点击监听 lcDevice.setItemClickedListener((listContainer, component, position, id) -> { //设置设备 bleCore.setDevice(mList.get(position).getDevice()); Intent jumpIntent = new Intent(); Operation operation = new Intent.OperationBuilder() .withAction("action.main") .withDeviceId("") .withBundleName("com.llw.ble") .withAbilityName("com.llw.ble.MainAbility") .build(); jumpIntent.setOperation(operation); startAbility(jumpIntent); });
这里跳转的代码比较多,跳转之前设置了设备进去,这种方式也是官方推荐的方式。然后我们修改MainAbility
中的代码,如下所示:
public class MainAbilitySlice extends AbilitySlice implements BleCallback { private static final String TAG = MainAbilitySlice.class.getSimpleName(); private Text txDisconnect; private Text txDeviceInfo; private ListContainer lcService; private BleCore bleCore; @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); txDisconnect = (Text) findComponentById(ResourceTable.Id_tx_disconnect); txDeviceInfo = (Text) findComponentById(ResourceTable.Id_tx_device_info); lcService = (ListContainer) findComponentById(ResourceTable.Id_lc_service); bleCore = BleApp.getBleCore(); bleCore.setBleCallback(this); //连接设备 bleCore.connect(); txDisconnect.setClickedListener(component -> { if (bleCore.isConnected()) { bleCore.disconnect(); } else { bleCore.connect(); } }); } @Override public void deviceInfo(String info) { getUITaskDispatcher().asyncDispatch(() -> { LogUtils.LogD(TAG, info); txDeviceInfo.setText(info); }); } @Override public void connectionStateChange(boolean state) { getUITaskDispatcher().asyncDispatch(() -> txDisconnect.setText(state ? "断开连接" : "连接")); } @Override public void servicesDiscovered(List<GattService> services) { } }
使用BleCore
的connect()
方法进行连接设备,在onStart()
方法中进行BleCore
的赋值,然后设置Ble的回调,实现BleCallback
接口,重写里面的函数,当连接成功之后会通过回调deviceInfo()
得到设备状态,因为是子线程所以在ui线程中渲染UI。而connectionStateChange()
函数,回调连接成功或者失败,如果成功则为ture,就显示txDisconnect
控件,此时连接成功,点击这个txDisconnect
就会断开连接,点击监听就在onStart()
中写好了,下面我们运行一下看看效果。
从这个效果图来看,我们连接成功之后有状态,点击断开连接也会有状态改变,那么连接就写好了。
四、发现服务
连接写好了,下面可以写发现服务了,我们可以在连接成功的处理中进行发现服务,下面我们修改一下BleDeviceCallback
中的connectionStateChangeEvent()
方法中的代码,如下图所示:
通过mDevice.discoverServices()
进行发现服务的动作,在此之前通过deviceInfo()
设置当前的动作状态,发现服务执行会触发servicesDiscoveredEvent()
回调,在这个回调中我们可以回调到页面,修改代码如下所示:
@Override public void servicesDiscoveredEvent(int status) { if (status == BlePeripheralDevice.OPERATION_SUCC) { deviceInfo("发现" + mDevice.getServices().size() + "服务"); if (bleCallback != null) { bleCallback.servicesDiscovered(mDevice.getServices()); } } }
在回调中设置发现服务的个数,然后回调,因为服务是多个的,那么下面我们就需要使用一个列表是装载服务,之前我们就已经在ability_main.xml
中写好了。
五、服务提供者
要显示服务列表数据,首先需要一个提供者,而提供者又需要一个item去渲染数据,下面我们在layout下创建一个item_service.xml
,代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_content" ohos:width="match_parent" ohos:background_element="$color:white" ohos:bottom_margin="2vp" ohos:bottom_padding="8vp" ohos:end_padding="16vp" ohos:start_padding="16vp" ohos:top_padding="8vp"> <Text ohos:id="$+id:tx_service_name" ohos:height="match_content" ohos:width="match_content" ohos:text="服务" ohos:text_size="16fp"/> <Button ohos:id="$+id:tx_uuid_title" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_service_name" ohos:text="UUID:" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Button ohos:id="$+id:tx_uuid" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_service_name" ohos:end_of="$id:tx_uuid_title" ohos:text="UUID" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Button ohos:id="$+id:tx_service_info" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_uuid_title" ohos:text="PRIMARY SERVICE" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> </DependentLayout>
下面我们在ble包下新建一个BleUtils
类,代码如下所示:
public class BleUtils { public static final String generic = "-0000-1000-8000-00805F9B34FB"; public static String getServiceUUID(UUID uuid) { return "0x" + uuid.toString().substring(4, 8).toUpperCase(); } /** * 获取蓝牙服务名称 * * @param uuid UUID */ public static String getServiceName(UUID uuid) { String targetUuid = getServiceUUID(uuid); switch (targetUuid) { case "0x1800": return "Generic Access service"; case "0x1801": return "Generic Attribute service"; case "0x1802": return "Immediate Alert service"; case "0x1803": return "Link Loss service"; case "0x1804": return "Tx Power service"; case "0x1805": return "Current Time service"; case "0x1806": return "Reference Time Update service"; case "0x1807": return "Next DST Change service"; case "0x1808": return "Glucose service"; case "0x1809": return "Health Thermometer service"; case "0x180A": return "Device Information service"; case "0x180D": return "Heart Rate service"; case "0x180E": return "Phone Alert Status service"; case "0x180F": return "Battery service"; case "0x1810": return "Blood Pressure service"; case "0x1811": return "Alert Notification service"; case "0x1812": return "Human Interface Device service"; case "0x1813": return "Scan Parameters service"; case "0x1814": return "Running Speed and Cadence service"; case "0x1815": return "Automation IO service"; case "0x1816": return "Cycling Speed and Cadence service"; case "0x1818": return "Cycling Power service"; case "0x1819": return "Location and Navigation service"; case "0x181A": return "Environmental Sensing service"; case "0x181B": return "Body Composition service"; case "0x181C": return "User Data service"; case "0x181D": return "Weight Scale service"; case "0x181E": return "Bond Management service"; case "0x181F": return "Continuous Glucose Monitoring service"; case "0x1820": return "Internet Protocol Support service"; case "0x1821": return "Indoor Positioning service"; case "0x1822": return "Pulse Oximeter service"; case "0x1823": return "HTTP Proxy service"; case "0x1824": return "Transport Discovery service"; case "0x1825": return "Object Transfer service"; case "0x1826": return "Fitness Machine service"; case "0x1827": return "Mesh Provisioning service"; case "0x1828": return "Mesh Proxy service"; case "0x1829": return "Reconnection Configuration service"; case "0x183A": return "Insulin Delivery service"; case "0x183B": return "Binary Sensor service"; case "0x183C": return "Emergency Configuration service"; case "0x183D": return "Authorization Control service"; case "0x183E": return "Physical Activity Monitor service"; case "0x183F": return "Elapsed Time service"; case "0x1840": return "Generic Health Sensor service"; case "0x1843": return "Audio Input Control service"; case "0x1844": return "Volume Control service"; case "0x1845": return "Volume Offset Control service"; case "0x1846": return "Coordinated Set Identification service"; case "0x1847": return "Device Time service"; case "0x1848": return "Media Control service"; case "0x1849": return "Generic Media Control service"; case "0x184A": return "Constant Tone Extension service"; case "0x184B": return "Telephone Bearer service"; case "0x184C": return "Generic Telephone Bearer service"; case "0x184D": return "Microphone Control service"; case "0x184E": return "Audio Stream Control service"; case "0x184F": return "Broadcast Audio Scan service"; case "0x1850": return " Published Audio Capabilities service"; case "0x1851": return "Basic Audio Announcement service"; case "0x1852": return "Broadcast Audio Announcement service"; case "0x1853": return "Common Audio service"; case "0x1854": return "Hearing Access service"; case "0x1855": return "Telephony and Media Audio service"; case "0x1856": return "Public Broadcast Announcement service"; case "0x1857": return "Electronic Shelf Label service"; default: return "Unknown Service"; } } }
这里需要说明一下蓝牙的UUID,蓝牙UUID(Universally Unique Identifier)
是用于唯一标识蓝牙设备和服务的一种标识符。它是一个128位长的数字,在蓝牙通信中起到唯一标识的作用。蓝牙UUID按照标准分为两种类型:
- 16位UUID:这些UUID通常用于蓝牙标准定义的一些通用服务和特性。例如,设备名称服务的UUID是 00001800-0000-1000-8000-00805F9B34FB。
- 128位UUID:这些UUID通常用于自定义的服务和特性,以确保全球唯一性。可以自行生成一个128位的UUID作为自定义的服务或特性标识。例如,一个自定义的服务UUID可以是 0000XXXX-0000-1000-8000-00805F9B34FB,其中的 XXXX 部分可以是任意的16进制数字。
在蓝牙通信中,设备使用UUID来发布和查找服务以及识别特性。UUID是蓝牙设备之间进行通信时的重要标识,确保了设备和服务的唯一性。
那么getServiceName()
中的键你就知道是什么意思了,0x1800就是16进制数字,而对应的值则是SIG
定义的,可以参考这个文档:Assigned_Numbers.pdf。如果你的值找不到对应的,那说明它不是SIG规范的,你这个服务UUID就是自己公司自定义的。
下面我们写提供者,在provider
包下新建一个ServiceProvider
类,代码如下所示:
public class ServiceProvider extends BaseItemProvider { private final List<GattService> serviceList; private final AbilitySlice slice; public ServiceProvider(List<GattService> list, AbilitySlice slice) { this.serviceList = list; this.slice = slice; } @Override public int getCount() { return serviceList == null ? 0 : serviceList.size(); } @Override public Object getItem(int position) { if (serviceList != null && position >= 0 && position < serviceList.size()) { return serviceList.get(position); } return null; } @Override public long getItemId(int position) { return position; } @Override public Component getComponent(int position, Component component, ComponentContainer componentContainer) { final Component cpt; ServiceHolder holder; GattService service = serviceList.get(position); if (component == null) { cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_service, null, false); holder = new ServiceHolder(cpt); //将获取到的子组件信息绑定到列表项的实例中 cpt.setTag(holder); } else { cpt = component; // 从缓存中获取到列表项实例后,直接使用绑定的子组件信息进行数据填充。 holder = (ServiceHolder) cpt.getTag(); } holder.txServiceName.setText(BleUtils.getServiceName(service.getUuid())); holder.txUuid.setText(BleUtils.getServiceUUID(service.getUuid())); return cpt; } /** * 用于保存列表项的子组件信息 */ public static class ServiceHolder { Text txServiceName; Text txUuid; public ServiceHolder(Component component) { txServiceName = (Text) component.findComponentById(ResourceTable.Id_tx_service_name); txUuid = (Text) component.findComponentById(ResourceTable.Id_tx_uuid); } } }
这里的代码就是比较简单的,就是基本的写法,下面回到MainAbilitySlice
中进行显示数据。
六、显示服务
首先声明变量:
private final List<GattService> serviceList = new ArrayList<>(); private ServiceProvider serviceProvider;
然后实现OnItemClickListener
接口
public class MainAbilitySlice extends AbilitySlice implements BleCallback, ListContainer.ItemClickedListener {
重写onItemClicked()
方法,并且增加了一个showMsg
,这个Toast的可定制化做的很多。
@Override public void onItemClicked(ListContainer listContainer, Component component, int position, long id) { showMsg(serviceList.get(position).getUuid().toString()); } private void showMsg(String msg) { ToastDialog toastDialog = new ToastDialog(getContext()); toastDialog.setSize(DirectionalLayout.LayoutConfig.MATCH_CONTENT, DirectionalLayout.LayoutConfig.MATCH_CONTENT); toastDialog.setDuration(2000); toastDialog.setText(msg); toastDialog.setAlignment(LayoutAlignment.CENTER); Text toastText = (Text) toastDialog.getComponent(); if (toastText != null) { toastText.setMultipleLine(true); toastText.setTextSize(14, Text.TextSizeType.FP); toastText.setTextColor(Color.WHITE); toastText.setPadding(40, 20, 40, 20); ShapeElement toastBackground = new ShapeElement(); toastBackground.setRgbColor(new RgbColor(24, 196, 124)); toastBackground.setCornerRadius(60f); toastText.setBackground(toastBackground); } toastDialog.show(); }
然后在onStart()
方法中初始化服务提供者,代码如下所示:
@Override public void onStart(Intent intent) { ... serviceProvider = new ServiceProvider(serviceList, this); lcService.setItemProvider(serviceProvider); lcService.setItemClickedListener(this); }
修改servicesDiscovered()
方法,代码如下所示:
@Override public void servicesDiscovered(List<GattService> services) { getUITaskDispatcher().asyncDispatch(() -> { serviceList.clear(); serviceList.addAll(services); serviceProvider.notifyDataChanged(); }); }
这里的写法其实和扫描设备哪里如出一辙,下面我们运行一下看看,什么效果。
七、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:HarmonyBle-Java